├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── _screenshots ├── desktop_frontpage 2015-06-22.jpg ├── desktop_submission 2015-06-22.jpg ├── mobile_frontpage 2015-06-22.png ├── mobile_submit 2015-06-22.png ├── mobile_thread 2015-06-22.png ├── profile_edit 2015-06-24.png └── profile_view 2015-06-24.png ├── comments ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── tests │ └── __init__.py ├── urls.py └── views.py ├── django_reddit ├── __init__.py ├── settings │ ├── __init__.py │ ├── common.py │ ├── local.py │ └── production.py ├── setup.cfg ├── urls.py ├── utils │ ├── __init__.py │ └── model_utils.py └── wsgi.py ├── manage.py ├── reddit ├── __init__.py ├── admin.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populate_test_data.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── test_comments.py │ ├── test_frontpage.py │ ├── test_profile.py │ ├── test_submission.py │ └── test_voting.py ├── urls.py ├── utils │ ├── __init__.py │ └── helpers.py └── views.py ├── requirements.txt ├── requirements ├── base.txt ├── local.txt └── production.txt ├── static ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── font-awesome.css │ ├── font-awesome.min.css │ └── reddit.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ └── sprite-reddit.EMWQffWtZwo.png └── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-1.11.1.min.js │ ├── npm.js │ └── reddit.js ├── staticfiles └── .gitkeep ├── submissions ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── tests │ └── __init__.py ├── urls.py └── views.py ├── templates ├── __items │ └── comment.html ├── __layout │ ├── footer.html │ └── navbar.html ├── base.html ├── private │ └── edit_profile.html └── public │ ├── comments.html │ ├── frontpage.html │ ├── login.html │ ├── profile.html │ ├── register.html │ └── submit.html └── users ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── tests ├── __init__.py ├── test_login.py ├── test_logout.py └── test_registration.py ├── urls.py └── views.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.py] 16 | line_length=120 17 | known_first_party={{ cookiecutter.repo_name }} 18 | multi_line_output=3 19 | default_section=THIRDPARTY 20 | 21 | [*.{html,css,scss,json,yml}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | [Makefile] 29 | indent_style = tab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,django,osx,linux,pycharm 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | 66 | ### Django ### 67 | *.log 68 | *.pot 69 | *.pyc 70 | __pycache__/ 71 | local_settings.py 72 | db.sqlite3 73 | media 74 | 75 | 76 | ### OSX ### 77 | .DS_Store 78 | .AppleDouble 79 | .LSOverride 80 | 81 | # Icon must end with two \r 82 | Icon 83 | 84 | 85 | # Thumbnails 86 | ._* 87 | 88 | # Files that might appear in the root of a volume 89 | .DocumentRevisions-V100 90 | .fseventsd 91 | .Spotlight-V100 92 | .TemporaryItems 93 | .Trashes 94 | .VolumeIcon.icns 95 | 96 | # Directories potentially created on remote AFP share 97 | .AppleDB 98 | .AppleDesktop 99 | Network Trash Folder 100 | Temporary Items 101 | .apdisk 102 | 103 | 104 | ### Linux ### 105 | *~ 106 | 107 | # KDE directory preferences 108 | .directory 109 | 110 | # Linux trash folder which might appear on any partition or disk 111 | .Trash-* 112 | 113 | 114 | ### PyCharm ### 115 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 116 | 117 | *.iml 118 | 119 | ## Directory-based project format: 120 | .idea/ 121 | # if you remove the above rule, at least ignore the following: 122 | 123 | # User-specific stuff: 124 | # .idea/workspace.xml 125 | # .idea/tasks.xml 126 | # .idea/dictionaries 127 | # .idea/shelf 128 | 129 | # Sensitive or high-churn files: 130 | # .idea/dataSources.ids 131 | # .idea/dataSources.xml 132 | # .idea/sqlDataSources.xml 133 | # .idea/dynamic.xml 134 | # .idea/uiDesigner.xml 135 | 136 | # Gradle: 137 | # .idea/gradle.xml 138 | # .idea/libraries 139 | 140 | # Mongo Explorer plugin: 141 | # .idea/mongoSettings.xml 142 | 143 | ## File-based project format: 144 | *.ipr 145 | *.iws 146 | 147 | ## Plugin-specific files: 148 | 149 | # IntelliJ 150 | /out/ 151 | 152 | # mpeltonen/sbt-idea plugin 153 | .idea_modules/ 154 | 155 | # JIRA plugin 156 | atlassian-ide-plugin.xml 157 | 158 | # Crashlytics plugin (for Android Studio and IntelliJ) 159 | com_crashlytics_export_strings.xml 160 | crashlytics.properties 161 | crashlytics-build.properties 162 | fabric.properties 163 | 164 | # Development sqlite database 165 | 166 | dev.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install -r requirements/local.txt 7 | - pip install coveralls 8 | script: 9 | - python manage.py test 10 | - coverage run --source=reddit manage.py test 11 | after_success: coveralls 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn django_reddit.wsgi:application 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Django reddit 2 | Reddit clone written in python using django web framework and twitter's bootstrap. 3 | 4 | [![Build Status](https://travis-ci.org/Nikola-K/django_reddit.svg)](https://travis-ci.org/Nikola-K/django_reddit) [![Coverage Status](https://coveralls.io/repos/Nikola-K/django_reddit/badge.svg?branch=master&service=github)](https://coveralls.io/github/Nikola-K/django_reddit?branch=master) 5 | 6 | #Screenshots 7 | 8 | ![desktop_frontpage](_screenshots/desktop_frontpage 2015-06-22.jpg?raw=true) 9 | 10 | ![desktop_submission](_screenshots/desktop_submission 2015-06-22.jpg?raw=true) 11 | 12 | ![profile_view](_screenshots/profile_view 2015-06-24.png) 13 | 14 | ![profile_edit](_screenshots/profile_edit 2015-06-24.png) 15 | 16 | Fully responsive: 17 | 18 | ![mobile_frontpage](_screenshots/mobile_frontpage 2015-06-22.png?raw=true) 19 | 20 | ![mobile_submit](_screenshots/mobile_submit 2015-06-22.png?raw=true) 21 | 22 | ![mobile_thread](_screenshots/mobile_thread 2015-06-22.png?raw=true) 23 | 24 | #Getting up and running 25 | 26 | The project is python 3 only. 27 | 28 | The steps below will get you up and running with a local development environment. We assume you have the following installed: 29 | 30 | pip 31 | virtualenv 32 | 33 | First make sure to create and activate a virtualenv, then open a terminal at the project root and install the requirements for local development: 34 | 35 | $ pip install -r requirements.txt 36 | $ python manage.py migrate 37 | $ python manage.py syncdb 38 | $ python manage.py runserver 39 | 40 | For the time being there is no separate production specific settings because the project is not yet production ready. 41 | 42 | #Deployment 43 | 44 | * TODO: Write here how to deploy 45 | 46 | #License 47 | 48 | Copyright 2016 Nikola Kovacevic 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, 58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 59 | See the License for the specific language governing permissions and 60 | limitations under the License. 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /_screenshots/desktop_frontpage 2015-06-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/desktop_frontpage 2015-06-22.jpg -------------------------------------------------------------------------------- /_screenshots/desktop_submission 2015-06-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/desktop_submission 2015-06-22.jpg -------------------------------------------------------------------------------- /_screenshots/mobile_frontpage 2015-06-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/mobile_frontpage 2015-06-22.png -------------------------------------------------------------------------------- /_screenshots/mobile_submit 2015-06-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/mobile_submit 2015-06-22.png -------------------------------------------------------------------------------- /_screenshots/mobile_thread 2015-06-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/mobile_thread 2015-06-22.png -------------------------------------------------------------------------------- /_screenshots/profile_edit 2015-06-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/profile_edit 2015-06-24.png -------------------------------------------------------------------------------- /_screenshots/profile_view 2015-06-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/_screenshots/profile_view 2015-06-24.png -------------------------------------------------------------------------------- /comments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/comments/__init__.py -------------------------------------------------------------------------------- /comments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /comments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommentsConfig(AppConfig): 5 | name = 'comments' 6 | -------------------------------------------------------------------------------- /comments/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/comments/forms.py -------------------------------------------------------------------------------- /comments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/comments/migrations/__init__.py -------------------------------------------------------------------------------- /comments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /comments/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/comments/tests/__init__.py -------------------------------------------------------------------------------- /comments/urls.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/comments/urls.py -------------------------------------------------------------------------------- /comments/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /django_reddit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/django_reddit/__init__.py -------------------------------------------------------------------------------- /django_reddit/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/django_reddit/settings/__init__.py -------------------------------------------------------------------------------- /django_reddit/settings/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Django settings for django_reddit project. 4 | 5 | 6 | For the full list of settings and their values, see 7 | https://docs.djangoproject.com/en/dev/ref/settings/ 8 | """ 9 | import os 10 | import environ 11 | 12 | ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) 13 | 14 | env = environ.Env() 15 | 16 | # APP CONFIGURATION 17 | # ------------------------------------------------------------------------------ 18 | DJANGO_APPS = ( 19 | # Default Django apps: 20 | 'django.contrib.auth', 21 | 'django.contrib.contenttypes', 22 | 'django.contrib.sessions', 23 | 'django.contrib.sites', 24 | 'django.contrib.messages', 25 | 'django.contrib.staticfiles', 26 | 27 | # Useful template tags: 28 | 'django.contrib.humanize', 29 | 30 | # Admin 31 | 'django.contrib.admin', 32 | ) 33 | THIRD_PARTY_APPS = ( 34 | 'widget_tweaks', 35 | 'mptt', 36 | # Insert your thirdy party apps here 37 | ) 38 | 39 | # Apps specific for this project go here. 40 | LOCAL_APPS = ( 41 | 'users', 42 | 'reddit', 43 | 'comments', 44 | 'submissions' 45 | # Your stuff: custom apps go here 46 | ) 47 | 48 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 49 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 50 | 51 | # MIDDLEWARE CONFIGURATION 52 | # ------------------------------------------------------------------------------ 53 | MIDDLEWARE_CLASSES = ( 54 | # Make sure djangosecure.middleware.SecurityMiddleware is listed first 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.messages.middleware.MessageMiddleware', 60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 61 | ) 62 | 63 | 64 | # DEBUG 65 | # ------------------------------------------------------------------------------ 66 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug 67 | DEBUG = env.bool("DJANGO_DEBUG", False) 68 | 69 | # FIXTURE CONFIGURATION 70 | # ------------------------------------------------------------------------------ 71 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS 72 | FIXTURE_DIRS = ( 73 | str(ROOT_DIR.path('fixtures')), 74 | ) 75 | 76 | # EMAIL CONFIGURATION 77 | # ------------------------------------------------------------------------------ 78 | EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') 79 | 80 | # MANAGER CONFIGURATION 81 | # ------------------------------------------------------------------------------ 82 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#admins 83 | ADMINS = ( 84 | ("""""", ''), 85 | ) 86 | 87 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#managers 88 | MANAGERS = ADMINS 89 | 90 | # DATABASE CONFIGURATION 91 | # ------------------------------------------------------------------------------ 92 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases 93 | DATABASES = { 94 | # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ 95 | 'default': env.db("DATABASE_URL", default="sqlite:///{}".format(ROOT_DIR + 'dev.db')), 96 | } 97 | DATABASES['default']['ATOMIC_REQUESTS'] = True 98 | 99 | 100 | # GENERAL CONFIGURATION 101 | # ------------------------------------------------------------------------------ 102 | # Local time zone for this installation. Choices can be found here: 103 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 104 | # although not all choices may be available on all operating systems. 105 | # In a Windows environment this must be set to your system time zone. 106 | TIME_ZONE = 'UTC' 107 | 108 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id 112 | SITE_ID = 1 113 | 114 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 115 | USE_I18N = True 116 | 117 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 118 | USE_L10N = True 119 | 120 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 121 | USE_TZ = True 122 | 123 | # TEMPLATE CONFIGURATION 124 | # ------------------------------------------------------------------------------ 125 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#templates 126 | TEMPLATES = [ 127 | { 128 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 129 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 130 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 131 | 'DIRS' : [ 132 | str(ROOT_DIR.path('templates')), 133 | ], 134 | 'OPTIONS': { 135 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug 136 | 'debug' : DEBUG, 137 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 138 | # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 139 | 'loaders' : [ 140 | 'django.template.loaders.filesystem.Loader', 141 | 'django.template.loaders.app_directories.Loader', 142 | ], 143 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 144 | 'context_processors': [ 145 | 'django.template.context_processors.debug', 146 | 'django.template.context_processors.request', 147 | 'django.contrib.auth.context_processors.auth', 148 | 'django.template.context_processors.i18n', 149 | 'django.template.context_processors.media', 150 | 'django.template.context_processors.static', 151 | 'django.template.context_processors.tz', 152 | 'django.contrib.messages.context_processors.messages', 153 | # Your stuff: custom template context processors go here 154 | ], 155 | }, 156 | }, 157 | ] 158 | 159 | 160 | # STATIC FILE CONFIGURATION 161 | # ------------------------------------------------------------------------------ 162 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root 163 | STATIC_ROOT = str(ROOT_DIR('staticfiles')) 164 | 165 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url 166 | STATIC_URL = '/static/' 167 | 168 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 169 | STATICFILES_DIRS = ( 170 | str(ROOT_DIR.path('static')), 171 | 'static', 172 | ) 173 | 174 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 175 | STATICFILES_FINDERS = ( 176 | 'django.contrib.staticfiles.finders.FileSystemFinder', 177 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 178 | ) 179 | 180 | # MEDIA CONFIGURATION 181 | # ------------------------------------------------------------------------------ 182 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root 183 | MEDIA_ROOT = str(ROOT_DIR('media')) 184 | 185 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url 186 | MEDIA_URL = '/media/' 187 | 188 | # URL Configuration 189 | # ------------------------------------------------------------------------------ 190 | ROOT_URLCONF = 'django_reddit.urls' 191 | 192 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 193 | WSGI_APPLICATION = 'django_reddit.wsgi.application' 194 | 195 | # Location of root django.contrib.admin URL, 196 | ADMIN_URL = r'^admin/' 197 | 198 | # See: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model 199 | # AUTH_USER_MODEL = 'users.Person' 200 | 201 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#login-url 202 | 203 | LOGIN_URL = '/login/' 204 | 205 | # Your common stuff: Below this line define 3rd party library settings 206 | -------------------------------------------------------------------------------- /django_reddit/settings/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Local settings 4 | 5 | - Run in Debug mode 6 | - Use console backend for emails 7 | - Add Django Debug Toolbar 8 | - Add django-extensions as app 9 | ''' 10 | 11 | from .common import * # noqa 12 | 13 | # DEBUG 14 | # ------------------------------------------------------------------------------ 15 | DEBUG = env.bool('DJANGO_DEBUG', default=True) 16 | TEMPLATES[0]['OPTIONS']['debug'] = DEBUG 17 | 18 | # SECRET CONFIGURATION 19 | # ------------------------------------------------------------------------------ 20 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 21 | # Note: This key only used for development and testing. 22 | SECRET_KEY = env("DJANGO_SECRET_KEY", default='CHANGEME!!!') 23 | 24 | # Mail settings 25 | # ------------------------------------------------------------------------------ 26 | EMAIL_HOST = 'localhost' 27 | EMAIL_PORT = 1025 28 | EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', 29 | default='django.core.mail.backends.console.EmailBackend') 30 | 31 | # CACHING 32 | # ------------------------------------------------------------------------------ 33 | CACHES = { 34 | 'default': { 35 | 'BACKEND' : 'django.core.cache.backends.locmem.LocMemCache', 36 | 'LOCATION': '' 37 | } 38 | } 39 | 40 | # django-debug-toolbar 41 | # ------------------------------------------------------------------------------ 42 | MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) 43 | INSTALLED_APPS += ('debug_toolbar',) 44 | 45 | INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',) 46 | 47 | DEBUG_TOOLBAR_CONFIG = { 48 | 'DISABLE_PANELS' : [ 49 | 'debug_toolbar.panels.redirects.RedirectsPanel', 50 | ], 51 | 'SHOW_TEMPLATE_CONTEXT': True, 52 | } 53 | 54 | 55 | # TESTING 56 | # ------------------------------------------------------------------------------ 57 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 58 | # Your local stuff: Below this line define 3rd party library settings 59 | -------------------------------------------------------------------------------- /django_reddit/settings/production.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Production Configurations 4 | 5 | - Use djangosecure 6 | - Use mailgun to send emails 7 | ''' 8 | from django.utils import six 9 | 10 | from .common import * # noqa 11 | 12 | # SECRET CONFIGURATION 13 | # ------------------------------------------------------------------------------ 14 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 15 | # Raises ImproperlyConfigured exception if DJANGO_SECRET_KEY not in os.environ 16 | SECRET_KEY = env("DJANGO_SECRET_KEY") 17 | 18 | # This ensures that Django will be able to detect a secure connection 19 | # properly on Heroku. 20 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 21 | 22 | # django-secure 23 | # ------------------------------------------------------------------------------ 24 | INSTALLED_APPS += ("djangosecure", ) 25 | 26 | SECURITY_MIDDLEWARE = ( 27 | 'djangosecure.middleware.SecurityMiddleware', 28 | ) 29 | 30 | MIDDLEWARE_CLASSES = () 31 | # Make sure djangosecure.middleware.SecurityMiddleware is listed first 32 | MIDDLEWARE_CLASSES = SECURITY_MIDDLEWARE + MIDDLEWARE_CLASSES 33 | 34 | # set this to 60 seconds and then to 518400 when you can prove it works 35 | SECURE_HSTS_SECONDS = 60 36 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 37 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) 38 | SECURE_FRAME_DENY = env.bool("DJANGO_SECURE_FRAME_DENY", default=True) 39 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 40 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) 41 | SECURE_BROWSER_XSS_FILTER = True 42 | SESSION_COOKIE_SECURE = False 43 | SESSION_COOKIE_HTTPONLY = True 44 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 45 | 46 | # SITE CONFIGURATION 47 | # ------------------------------------------------------------------------------ 48 | # Hosts/domain names that are valid for this site 49 | # See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts 50 | ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['example.com']) 51 | # END SITE CONFIGURATION 52 | 53 | INSTALLED_APPS += ("gunicorn", ) 54 | 55 | 56 | # EMAIL 57 | # ------------------------------------------------------------------------------ 58 | DEFAULT_FROM_EMAIL = env('DJANGO_DEFAULT_FROM_EMAIL', 59 | default='django_reddit 5: 86 | return 87 | 88 | comment_author = self.get_or_create_author(choice(self.random_usernames)) 89 | 90 | raw_text = self.get_random_sentence() 91 | new_comment = Comment.create(comment_author, raw_text, root_comment) 92 | new_comment.save() 93 | if choice([True, False]): 94 | self.add_replies(new_comment, depth + 1) 95 | -------------------------------------------------------------------------------- /reddit/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-23 20:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django.db.models.manager 8 | import django.utils.timezone 9 | import mptt.fields 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('contenttypes', '0002_remove_content_type_name'), 18 | ('users', '0001_initial'), 19 | ] 20 | 21 | operations = [ 22 | migrations.CreateModel( 23 | name='Comment', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('author_name', models.CharField(max_length=12)), 27 | ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), 28 | ('ups', models.IntegerField(default=0)), 29 | ('downs', models.IntegerField(default=0)), 30 | ('score', models.IntegerField(default=0)), 31 | ('raw_comment', models.TextField(blank=True)), 32 | ('html_comment', models.TextField(blank=True)), 33 | ('lft', models.PositiveIntegerField(db_index=True, editable=False)), 34 | ('rght', models.PositiveIntegerField(db_index=True, editable=False)), 35 | ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), 36 | ('level', models.PositiveIntegerField(db_index=True, editable=False)), 37 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.RedditUser')), 38 | ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='reddit.Comment')), 39 | ], 40 | options={ 41 | 'abstract': False, 42 | }, 43 | managers=[ 44 | ('_default_manager', django.db.models.manager.Manager()), 45 | ], 46 | ), 47 | migrations.CreateModel( 48 | name='Submission', 49 | fields=[ 50 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 51 | ('author_name', models.CharField(max_length=12)), 52 | ('title', models.CharField(max_length=250)), 53 | ('url', models.URLField(blank=True, null=True)), 54 | ('text', models.TextField(blank=True, max_length=5000)), 55 | ('text_html', models.TextField(blank=True)), 56 | ('ups', models.IntegerField(default=0)), 57 | ('downs', models.IntegerField(default=0)), 58 | ('score', models.IntegerField(default=0)), 59 | ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), 60 | ('comment_count', models.IntegerField(default=0)), 61 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.RedditUser')), 62 | ], 63 | options={ 64 | 'abstract': False, 65 | }, 66 | ), 67 | migrations.CreateModel( 68 | name='vote', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('vote_object_id', models.PositiveIntegerField()), 72 | ('value', models.IntegerField(default=0)), 73 | ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reddit.Submission')), 74 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.RedditUser')), 75 | ('vote_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 76 | ], 77 | ), 78 | migrations.AddField( 79 | model_name='comment', 80 | name='submission', 81 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reddit.Submission'), 82 | ), 83 | ] 84 | -------------------------------------------------------------------------------- /reddit/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/reddit/migrations/__init__.py -------------------------------------------------------------------------------- /reddit/models.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | from django.contrib.auth.models import User 3 | from django.contrib.contenttypes.fields import GenericForeignKey 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.db import models 6 | from django.utils import timezone 7 | from mptt.models import MPTTModel, TreeForeignKey 8 | from django_reddit.utils.model_utils import ContentTypeAware, MttpContentTypeAware 9 | 10 | 11 | 12 | class Submission(ContentTypeAware): 13 | author_name = models.CharField(null=False, max_length=12) 14 | author = models.ForeignKey('users.RedditUser') 15 | title = models.CharField(max_length=250) 16 | url = models.URLField(null=True, blank=True) 17 | text = models.TextField(max_length=5000, blank=True) 18 | text_html = models.TextField(blank=True) 19 | ups = models.IntegerField(default=0) 20 | downs = models.IntegerField(default=0) 21 | score = models.IntegerField(default=0) 22 | timestamp = models.DateTimeField(default=timezone.now) 23 | comment_count = models.IntegerField(default=0) 24 | 25 | def generate_html(self): 26 | if self.text: 27 | html = mistune.markdown(self.text) 28 | self.text_html = html 29 | 30 | @property 31 | def linked_url(self): 32 | if self.url: 33 | return "{}".format(self.url) 34 | else: 35 | return "/comments/{}".format(self.id) 36 | 37 | @property 38 | def comments_url(self): 39 | return '/comments/{}'.format(self.id) 40 | 41 | def __unicode__(self): 42 | return "".format(self.id) 43 | 44 | 45 | class Comment(MttpContentTypeAware): 46 | author_name = models.CharField(null=False, max_length=12) 47 | author = models.ForeignKey('users.RedditUser') 48 | submission = models.ForeignKey(Submission) 49 | parent = TreeForeignKey('self', related_name='children', 50 | null=True, blank=True, db_index=True) 51 | timestamp = models.DateTimeField(default=timezone.now) 52 | ups = models.IntegerField(default=0) 53 | downs = models.IntegerField(default=0) 54 | score = models.IntegerField(default=0) 55 | raw_comment = models.TextField(blank=True) 56 | html_comment = models.TextField(blank=True) 57 | 58 | class MPTTMeta: 59 | order_insertion_by = ['-score'] 60 | 61 | @classmethod 62 | def create(cls, author, raw_comment, parent): 63 | """ 64 | Create a new comment instance. If the parent is submisison 65 | update comment_count field and save it. 66 | If parent is comment post it as child comment 67 | :param author: RedditUser instance 68 | :type author: RedditUser 69 | :param raw_comment: Raw comment text 70 | :type raw_comment: str 71 | :param parent: Comment or Submission that this comment is child of 72 | :type parent: Comment | Submission 73 | :return: New Comment instance 74 | :rtype: Comment 75 | """ 76 | 77 | html_comment = mistune.markdown(raw_comment) 78 | # todo: any exceptions possible? 79 | comment = cls(author=author, 80 | author_name=author.user.username, 81 | raw_comment=raw_comment, 82 | html_comment=html_comment) 83 | 84 | if isinstance(parent, Submission): 85 | submission = parent 86 | comment.submission = submission 87 | elif isinstance(parent, Comment): 88 | submission = parent.submission 89 | comment.submission = submission 90 | comment.parent = parent 91 | else: 92 | return 93 | submission.comment_count += 1 94 | submission.save() 95 | 96 | return comment 97 | 98 | def __unicode__(self): 99 | return "".format(self.id) 100 | 101 | 102 | class Vote(models.Model): 103 | user = models.ForeignKey('users.RedditUser') 104 | submission = models.ForeignKey(Submission) 105 | vote_object_type = models.ForeignKey(ContentType) 106 | vote_object_id = models.PositiveIntegerField() 107 | vote_object = GenericForeignKey('vote_object_type', 'vote_object_id') 108 | value = models.IntegerField(default=0) 109 | 110 | @classmethod 111 | def create(cls, user, vote_object, vote_value): 112 | """ 113 | Create a new vote object and return it. 114 | It will also update the ups/downs/score fields in the 115 | vote_object instance and save it. 116 | 117 | :param user: RedditUser instance 118 | :type user: RedditUser 119 | :param vote_object: Instance of the object the vote is cast on 120 | :type vote_object: Comment | Submission 121 | :param vote_value: Value of the vote 122 | :type vote_value: int 123 | :return: new Vote instance 124 | :rtype: Vote 125 | """ 126 | 127 | if isinstance(vote_object, Submission): 128 | submission = vote_object 129 | vote_object.author.link_karma += vote_value 130 | else: 131 | submission = vote_object.submission 132 | vote_object.author.comment_karma += vote_value 133 | 134 | vote = cls(user=user, 135 | vote_object=vote_object, 136 | value=vote_value) 137 | 138 | vote.submission = submission 139 | # the value for new vote will never be 0 140 | # that can happen only when removing up/down vote. 141 | vote_object.score += vote_value 142 | if vote_value == 1: 143 | vote_object.ups += 1 144 | elif vote_value == -1: 145 | vote_object.downs += 1 146 | 147 | vote_object.save() 148 | vote_object.author.save() 149 | 150 | return vote 151 | 152 | def change_vote(self, new_vote_value): 153 | if self.value == -1 and new_vote_value == 1: # down to up 154 | vote_diff = 2 155 | self.vote_object.score += 2 156 | self.vote_object.ups += 1 157 | self.vote_object.downs -= 1 158 | elif self.value == 1 and new_vote_value == -1: # up to down 159 | vote_diff = -2 160 | self.vote_object.score -= 2 161 | self.vote_object.ups -= 1 162 | self.vote_object.downs += 1 163 | elif self.value == 0 and new_vote_value == 1: # canceled vote to up 164 | vote_diff = 1 165 | self.vote_object.ups += 1 166 | self.vote_object.score += 1 167 | elif self.value == 0 and new_vote_value == -1: # canceled vote to down 168 | vote_diff = -1 169 | self.vote_object.downs += 1 170 | self.vote_object.score -= 1 171 | else: 172 | return None 173 | 174 | if isinstance(self.vote_object, Submission): 175 | self.vote_object.author.link_karma += vote_diff 176 | else: 177 | self.vote_object.author.comment_karma += vote_diff 178 | 179 | self.value = new_vote_value 180 | self.vote_object.save() 181 | self.vote_object.author.save() 182 | self.save() 183 | 184 | return vote_diff 185 | 186 | def cancel_vote(self): 187 | if self.value == 1: 188 | vote_diff = -1 189 | self.vote_object.ups -= 1 190 | self.vote_object.score -= 1 191 | elif self.value == -1: 192 | vote_diff = 1 193 | self.vote_object.downs -= 1 194 | self.vote_object.score += 1 195 | else: 196 | return None 197 | 198 | if isinstance(self.vote_object, Submission): 199 | self.vote_object.author.link_karma += vote_diff 200 | else: 201 | self.vote_object.author.comment_karma += vote_diff 202 | 203 | self.value = 0 204 | self.save() 205 | self.vote_object.save() 206 | self.vote_object.author.save() 207 | return vote_diff 208 | -------------------------------------------------------------------------------- /reddit/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/reddit/tests/__init__.py -------------------------------------------------------------------------------- /reddit/tests/test_comments.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.core.urlresolvers import reverse 3 | from django.test import TestCase, Client 4 | from reddit.models import Submission, Comment, Vote 5 | from users.models import RedditUser 6 | from django.contrib.auth.models import User 7 | from django.utils.crypto import get_random_string 8 | from django.http import HttpResponseNotAllowed, HttpResponseBadRequest 9 | 10 | 11 | class TestViewingThreadComments(TestCase): 12 | def setUp(self): 13 | self.c = Client() 14 | self.credentials = {'username': 'username', 15 | 'password': 'password'} 16 | author = RedditUser.objects.create( 17 | user=User.objects.create_user(**self.credentials) 18 | ) 19 | 20 | submission = Submission.objects.create( 21 | id=1, 22 | score=1, 23 | title=get_random_string(length=12), 24 | author=author 25 | ) 26 | 27 | for _ in range(3): 28 | Comment.objects.create( 29 | author_name=author.user.username, 30 | author=author, 31 | submission=submission, 32 | html_comment="root comment" 33 | ) 34 | 35 | # Add some replies 36 | parent = Comment.objects.get(id=1) 37 | for _ in range(2): 38 | Comment.objects.create( 39 | author_name=author.user.username, 40 | author=author, 41 | submission=submission, 42 | parent=parent, 43 | html_comment="reply comment" 44 | ) 45 | 46 | # add upvote to one root comment, 47 | Vote.create( 48 | user=author, 49 | vote_object=Comment.objects.get(id=1), 50 | vote_value=1 51 | ).save() 52 | 53 | # and downvote to one reply comment 54 | Vote.create( 55 | user=author, 56 | vote_object=Comment.objects.get(id=5), 57 | vote_value=-1 58 | ).save() 59 | 60 | # add upvote to the submission 61 | Vote.create( 62 | user=author, 63 | vote_object=submission, 64 | vote_value=1 65 | ).save() 66 | 67 | def test_valid_public_comment_view(self): 68 | self.c.logout() 69 | r = self.c.get(reverse('thread', args=(1,))) 70 | submission = Submission.objects.get(id=1) 71 | self.assertEqual(r.status_code, 200) 72 | self.assertEqual(r.context['submission'], submission) 73 | self.assertEqual(len(r.context['comments']), 5) 74 | self.assertContains(r, 'root comment', count=3) 75 | self.assertContains(r, 'reply comment', count=2) 76 | self.assertEqual(r.context['comment_votes'], {}) 77 | self.assertIsNone(r.context['sub_vote']) 78 | 79 | def test_comment_votes(self): 80 | self.c.login(**self.credentials) 81 | r = self.c.get(reverse('thread', args=(1,))) 82 | self.assertEqual(r.status_code, 200) 83 | self.assertEqual(r.context['sub_vote'], 1) 84 | self.assertEqual(r.context['comment_votes'], {1: 1, 5: -1}) 85 | self.assertContains(r, 'root comment', count=3) 86 | self.assertContains(r, 'reply comment', count=2) 87 | 88 | def test_invalid_thread_id(self): 89 | r = self.c.get(reverse('thread', args=(123,))) 90 | self.assertEqual(r.status_code, 404) 91 | 92 | 93 | class TestPostingComment(TestCase): 94 | def setUp(self): 95 | self.c = Client() 96 | self.credentials = {'username': 'commentposttest', 97 | 'password': 'password'} 98 | author = RedditUser.objects.create( 99 | user=User.objects.create_user(**self.credentials) 100 | ) 101 | 102 | Submission.objects.create( 103 | id=99, 104 | score=1, 105 | title=get_random_string(length=12), 106 | author=author 107 | ) 108 | 109 | def test_post_only(self): 110 | r = self.c.get(reverse('post_comment')) 111 | self.assertIsInstance(r, HttpResponseNotAllowed) 112 | 113 | def test_logged_out(self): 114 | r = self.c.post(reverse('post_comment')) 115 | self.assertEqual(r.status_code, 200) 116 | json_response = json.loads(r.content.decode("utf-8")) 117 | self.assertEqual(json_response['msg'], "You need to log in to post new comments.") 118 | 119 | def test_missing_type_or_id(self): 120 | self.c.login(**self.credentials) 121 | for key in ['parentType', 'parentId']: 122 | r = self.c.post(reverse('post_comment'), 123 | data={key: 'comment'}) 124 | self.assertIsInstance(r, HttpResponseBadRequest) 125 | r = self.c.post(reverse('post_comment'), 126 | data={'parentType': 'InvalidType', 127 | 'parentId': 1}) 128 | self.assertIsInstance(r, HttpResponseBadRequest) 129 | 130 | def test_no_comment_text(self): 131 | self.c.login(**self.credentials) 132 | test_data = { 133 | 'parentType': 'submission', 134 | 'parentId': 1, 135 | 'commentContent': '' 136 | } 137 | r = self.c.post(reverse('post_comment'), data=test_data) 138 | self.assertEqual(r.status_code, 200) 139 | json_response = json.loads(r.content.decode("utf-8")) 140 | self.assertEqual(json_response['msg'], 141 | 'You have to write something.') 142 | 143 | def test_invalid_or_wrong_parent_id(self): 144 | self.c.login(**self.credentials) 145 | test_data = { 146 | 'parentType': 'submission', 147 | 'parentId': 'invalid', 148 | 'commentContent': 'content' 149 | } 150 | r = self.c.post(reverse('post_comment'), data=test_data) 151 | self.assertIsInstance(r, HttpResponseBadRequest) 152 | 153 | test_data = { 154 | 'parentType': 'submission', 155 | 'parentId': 9999, 156 | 'commentContent': 'content' 157 | } 158 | 159 | r = self.c.post(reverse('post_comment'), data=test_data) 160 | self.assertIsInstance(r, HttpResponseBadRequest) 161 | 162 | test_data = { 163 | 'parentType': 'comment', 164 | 'parentId': 9999, 165 | 'commentContent': 'content' 166 | } 167 | 168 | r = self.c.post(reverse('post_comment'), data=test_data) 169 | self.assertIsInstance(r, HttpResponseBadRequest) 170 | 171 | def test_valid_comment_posting_thread(self): 172 | self.c.login(**self.credentials) 173 | test_data = { 174 | 'parentType': 'submission', 175 | 'parentId': 99, 176 | 'commentContent': 'thread root comment' 177 | } 178 | 179 | r = self.c.post(reverse('post_comment'), data=test_data) 180 | self.assertEqual(r.status_code, 200) 181 | json_r = json.loads(r.content.decode("utf-8")) 182 | self.assertEqual(json_r['msg'], 'Your comment has been posted.') 183 | all_comments = Comment.objects.filter( 184 | submission=Submission.objects.get(id=99) 185 | ) 186 | self.assertEqual(all_comments.count(), 1) 187 | comment = all_comments.first() 188 | self.assertEqual(comment.html_comment, '

thread root comment

\n') 189 | self.assertEqual(comment.author.user.username, self.credentials['username']) 190 | 191 | def test_valid_comment_posting_reply(self): 192 | self.c.login(**self.credentials) 193 | thread = Submission.objects.get(id=99) 194 | author = RedditUser.objects.get(user=User.objects.get( 195 | username=self.credentials['username'] 196 | )) 197 | comment = Comment.create(author, 'root comment', thread) 198 | comment.save() 199 | self.assertEqual(Comment.objects.filter(submission=thread).count(), 1) 200 | 201 | test_data = { 202 | 'parentType': 'comment', 203 | 'parentId': comment.id, 204 | 'commentContent': 'thread reply comment' 205 | } 206 | 207 | r = self.c.post(reverse('post_comment'), data=test_data) 208 | self.assertEqual(r.status_code, 200) 209 | json_r = json.loads(r.content.decode("utf-8")) 210 | self.assertEqual(json_r['msg'], 'Your comment has been posted.') 211 | self.assertEqual(Comment.objects.filter(submission=thread).count(), 2) 212 | 213 | comment = Comment.objects.filter(submission=thread, 214 | id=2).first() 215 | self.assertEqual(comment.html_comment, '

thread reply comment

\n') 216 | -------------------------------------------------------------------------------- /reddit/tests/test_frontpage.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | from reddit.models import Submission, Vote 4 | from django.contrib.auth.models import User 5 | from django.utils.crypto import get_random_string 6 | from users.models import RedditUser 7 | 8 | class TestFrontPageGET(TestCase): 9 | def setUp(self): 10 | self.c = Client() 11 | author = RedditUser.objects.create( 12 | user=User.objects.create_user(username="username", 13 | password="password")) 14 | 15 | for i in range(50): 16 | Submission.objects.create(score=i ** 2, 17 | title=get_random_string(length=20), 18 | author=author) 19 | 20 | def test_submission_count(self): 21 | self.assertEqual(Submission.objects.all().count(), 50) 22 | 23 | def test_no_page_number(self): 24 | r = self.c.get(reverse('frontpage')) 25 | self.assertEqual(len(r.context['submissions']), 25) 26 | self.assertEqual(r.context['submissions'].number, 1) 27 | self.assertFalse(r.context['submissions'].has_previous()) 28 | self.assertTrue(r.context['submissions'].has_next()) 29 | 30 | def test_valid_page_number(self): 31 | r = self.c.get(reverse('frontpage'), data={'page': 1}) 32 | self.assertEqual(len(r.context['submissions']), 25) 33 | first_page_submissions = r.context['submissions'] 34 | self.assertFalse(r.context['submissions'].has_previous()) 35 | 36 | r = self.c.get(reverse('frontpage'), data={'page': 2}) 37 | self.assertEqual(len(r.context['submissions']), 25) 38 | second_page_submissions = r.context['submissions'] 39 | self.assertNotEqual(first_page_submissions, second_page_submissions) 40 | 41 | self.assertFalse(r.context['submissions'].has_next()) 42 | 43 | def test_invalid_page_number(self): 44 | r = self.c.get(reverse('frontpage'), data={'page': "something"}, follow=True) 45 | self.assertEqual(r.status_code, 404) 46 | 47 | def test_wrong_page_number(self): 48 | r = self.c.get(reverse('frontpage'), data={'page': 10}, follow=True) 49 | self.assertEqual(r.context['submissions'].number, 2) 50 | self.assertFalse(r.context['submissions'].has_next()) 51 | 52 | self.assertTrue(r.context['submissions'].has_previous()) 53 | self.assertEqual(r.context['submissions'].previous_page_number(), 1) 54 | 55 | 56 | class TestFrontpageVotes(TestCase): 57 | def setUp(self): 58 | self.c = Client() 59 | author = RedditUser.objects.create( 60 | user=User.objects.create_user(username="username", 61 | password="password")) 62 | 63 | for i in range(50): 64 | Submission.objects.create(score=50 - i, 65 | title=get_random_string(length=20), 66 | author=author).save() 67 | 68 | for i in range(1, 50, 10): 69 | # [1, 11, 21] [31, 41] have upvotes (lists demonstrate pages) 70 | Vote.create(user=author, 71 | vote_object=Submission.objects.get(id=i), 72 | vote_value=1).save() 73 | 74 | for i in range(2, 50, 15): 75 | # [2, 17] [32, 47] have downvotes (lists demonstrate pages) 76 | Vote.create(user=author, 77 | vote_object=Submission.objects.get(id=i), 78 | vote_value=-1).save() 79 | 80 | def test_logged_out(self): 81 | r = self.c.get(reverse('frontpage')) 82 | self.assertEqual(r.context['submission_votes'], {}, 83 | msg="Logged out user got some submission votes data") 84 | 85 | def test_logged_in(self): 86 | self.c.login(username='username', password='password') 87 | r = self.c.get(reverse('frontpage')) 88 | self.assertEqual(len(r.context['submission_votes']), 5) 89 | 90 | upvote_keys = [] 91 | downvote_keys = [] 92 | for post_id, vote_value in list(r.context['submission_votes'].items()): 93 | if vote_value == 1: 94 | upvote_keys.append(post_id) 95 | elif vote_value == -1: 96 | downvote_keys.append(post_id) 97 | 98 | self.assertEqual(upvote_keys, [1, 11, 21], 99 | msg="Got wrong values for submission upvotes") 100 | self.assertEqual(downvote_keys, [2, 17], 101 | msg="Got wrong values for submission downvotes") 102 | 103 | def test_second_page(self): 104 | self.c.login(username='username', password='password') 105 | r = self.c.get(reverse('frontpage'), data={'page': 2}) 106 | self.assertEqual(len(r.context['submission_votes']), 4) 107 | 108 | upvote_keys = [] 109 | downvote_keys = [] 110 | for post_id, vote_value in list(r.context['submission_votes'].items()): 111 | if vote_value == 1: 112 | upvote_keys.append(post_id) 113 | elif vote_value == -1: 114 | downvote_keys.append(post_id) 115 | 116 | self.assertEqual(upvote_keys, [41, 31]) 117 | self.assertEqual(downvote_keys, [32, 47], 118 | msg="Got wrong values for submission downvotes") 119 | -------------------------------------------------------------------------------- /reddit/tests/test_profile.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | from django.contrib.auth.models import User 4 | from users.models import RedditUser 5 | from reddit.forms import ProfileForm 6 | 7 | 8 | class TestProfileViewing(TestCase): 9 | def setUp(self): 10 | self.c = Client() 11 | r = RedditUser.objects.create( 12 | user=User.objects.create_user( 13 | username="username", 14 | password="password" 15 | ) 16 | ) 17 | 18 | r.first_name = "First Name" 19 | r.last_name = "Last Name" 20 | r.about_html = "about html text" 21 | r.github = "example" 22 | 23 | def test_existing_username(self): 24 | r = self.c.get(reverse('user_profile', args=('username',))) 25 | self.assertEqual(r.status_code, 200) 26 | self.assertContains(r, '(username)') 27 | self.assertContains(r, 'compose?to=username') 28 | self.assertNotContains(r, '/profile/edit') 29 | self.assertNotContains(r, 'First Name') 30 | self.assertNotContains(r, 'Last Name') 31 | self.assertNotContains(r, 'about html text') 32 | self.assertNotContains(r, 'https://github.com/example') 33 | 34 | def test_own_username(self): 35 | self.assertTrue(self.c.login(username='username', password='password')) 36 | r = self.c.get(reverse('user_profile', args=('username',))) 37 | self.assertContains(r, '/profile/edit') 38 | self.assertNotContains(r, 'compose?to=username') 39 | self.c.logout() 40 | 41 | def test_invalid_username(self): 42 | r = self.c.get(reverse('user_profile', args=('none',))) 43 | self.assertEqual(r.status_code, 404) 44 | 45 | 46 | class TestProfileEditingForm(TestCase): 47 | def setUp(self): 48 | self.valid_data = { 49 | 'first_name': 'first_name', 50 | 'last_name': 'last_name', 51 | 'email': 'email@example.com', 52 | 'display_picture': False, 53 | 'about_text': 'about_text', 54 | 'homepage': 'http://example.com', 55 | 'github': 'username', 56 | 'twitter': 'username', 57 | } 58 | 59 | self.invalid_data = { 60 | 'first_name': 'too_long_first_name', 61 | 'last_name': 'too_long_last_name', 62 | 'email': 'notanemail', 63 | 'display_picture': False, 64 | 'about_text': 'toolong' * 75, 65 | 'homepage': 'notadomain', 66 | 'github': 'toolong' * 10, 67 | 'twitter': 'toolong' * 5, 68 | } 69 | 70 | def test_all_valid_data(self): 71 | form = ProfileForm(data=self.valid_data) 72 | self.assertTrue(form.is_valid()) 73 | 74 | def test_invalid_data(self): 75 | form = ProfileForm(data=self.invalid_data) 76 | self.assertEqual(form.errors['first_name'], ["Ensure this value has at most 12 characters (it has 19)."]) 77 | self.assertEqual(form.errors['last_name'], ["Ensure this value has at most 12 characters (it has 18)."]) 78 | self.assertEqual(form.errors['email'], ["Enter a valid email address."]) 79 | self.assertEqual(form.errors['about_text'], ["Ensure this value has at most 500 characters (it has 525)."]) 80 | self.assertEqual(form.errors['homepage'], ["Enter a valid URL."]) 81 | self.assertEqual(form.errors['github'], ["Ensure this value has at most 39 characters (it has 70)."]) 82 | self.assertEqual(form.errors['twitter'], ["Ensure this value has at most 15 characters (it has 35)."]) 83 | self.assertFalse(form.is_valid()) 84 | 85 | def test_empty_data(self): 86 | test_data = { 87 | 'first_name': '', 88 | 'last_name': '', 89 | 'email': '', 90 | 'display_picture': False, 91 | 'about_text': '', 92 | 'homepage': '', 93 | 'github': '', 94 | 'twitter': '', 95 | } 96 | form = ProfileForm(data=test_data) 97 | self.assertTrue(form.is_valid()) 98 | 99 | 100 | class TestProfilePageRequests(TestCase): 101 | def setUp(self): 102 | self.c = Client() 103 | RedditUser.objects.create( 104 | user=User.objects.create_user(username='profiletest', 105 | password='password') 106 | ) 107 | self.valid_data = { 108 | 'first_name': 'first_name', 109 | 'last_name': 'last_name', 110 | 'email': 'email@example.com', 111 | 'display_picture': True, 112 | 'about_text': 'about_text', 113 | 'homepage': 'http://example.com', 114 | 'github': 'username', 115 | 'twitter': 'username', 116 | } 117 | 118 | def test_not_logged_in(self): 119 | r = self.c.get(reverse('edit_profile')) 120 | self.assertRedirects(r, reverse('login') + '?next=' + reverse('edit_profile')) 121 | 122 | def test_invalid_request(self): 123 | self.c.login(username='profiletest', 124 | password='password') 125 | r = self.c.delete(reverse('edit_profile')) 126 | self.assertEqual(r.status_code, 404) 127 | 128 | def test_form_view(self): 129 | self.c.login(username='profiletest', 130 | password='password') 131 | r = self.c.get(reverse('edit_profile')) 132 | self.assertIsInstance(r.context['form'], ProfileForm) 133 | 134 | def test_form_submit(self): 135 | self.c.login(username='profiletest', 136 | password='password') 137 | 138 | r = self.c.post(reverse('edit_profile'), data=self.valid_data) 139 | self.assertEqual(r.status_code, 200) 140 | 141 | user = RedditUser.objects.get(user=User.objects.get( 142 | username='profiletest' 143 | )) 144 | for name, value in list(self.valid_data.items()): 145 | self.assertEqual(getattr(user, name), value) 146 | 147 | self.assertEqual(user.gravatar_hash, '5658ffccee7f0ebfda2b226238b1eb6e') 148 | self.assertEqual(user.about_html, '

about_text

\n') 149 | -------------------------------------------------------------------------------- /reddit/tests/test_submission.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.urlresolvers import reverse 3 | from django.test import TestCase, Client 4 | from reddit.forms import SubmissionForm 5 | from reddit.models import Submission 6 | from users.models import RedditUser 7 | 8 | class TestSubmissionForm(TestCase): 9 | def test_full_valid_submission(self): 10 | test_data = { 11 | 'title': 'submission_title', 12 | 'url': 'http://example.com', 13 | 'text': 'submission text' 14 | } 15 | form = SubmissionForm(data=test_data) 16 | self.assertTrue(form.is_valid()) 17 | 18 | def test_minimum_data_required(self): 19 | test_data = { 20 | 'title': 'submission title' 21 | } 22 | form = SubmissionForm(data=test_data) 23 | self.assertTrue(form.is_valid()) 24 | 25 | def test_invalid_data(self): 26 | test_data = { 27 | 'title': '.' * 300, 28 | 'url': 'notaurl', 29 | 'text': '.' * 5001 30 | } 31 | form = SubmissionForm(data=test_data) 32 | self.assertEqual(form.errors['title'], ["Ensure this value has at most 250 characters (it has 300)."]) 33 | self.assertEqual(form.errors['url'], ["Enter a valid URL."]) 34 | self.assertEqual(form.errors['text'], ["Ensure this value has at most 5000 characters (it has 5001)."]) 35 | self.assertFalse(form.is_valid()) 36 | 37 | 38 | class TestSubmissionRequests(TestCase): 39 | def setUp(self): 40 | self.c = Client() 41 | self.login_data = { 42 | 'username': 'submissiontest', 43 | 'password': 'password' 44 | } 45 | RedditUser.objects.create( 46 | user=User.objects.create_user(**self.login_data) 47 | ) 48 | 49 | def test_logged_out(self): 50 | r = self.c.get(reverse('submit')) 51 | self.assertRedirects(r, "{}?next={}".format( 52 | reverse('login'), reverse('submit') 53 | )) 54 | 55 | def test_logged_in_GET(self): 56 | self.c.login(**self.login_data) 57 | r = self.c.get(reverse('submit')) 58 | self.assertIsInstance(r.context['form'], SubmissionForm) 59 | 60 | def test_making_a_submission(self): 61 | self.c.login(**self.login_data) 62 | test_data = { 63 | 'title': 'submission title', 64 | 'url': 'http://example.com', 65 | 'text': 'submission text' 66 | } 67 | r = self.c.post(reverse('submit'), data=test_data, follow=True) 68 | submission = Submission.objects.filter(**test_data).first() 69 | self.assertIsNotNone(submission) 70 | self.assertRedirects(r, reverse('thread', args=(submission.id,))) 71 | self.assertContains(r, 'Submission created') 72 | 73 | def test_missing_fields(self): 74 | self.c.login(**self.login_data) 75 | 76 | test_data = { 77 | 'url': 'http://example.com', 78 | 'text': 'submission text' 79 | } 80 | r = self.c.post(reverse('submit'), data=test_data) 81 | self.assertNotContains(r, 'Submission created') 82 | self.assertContains(r, 'This field is required.') 83 | -------------------------------------------------------------------------------- /reddit/tests/test_voting.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.core.urlresolvers import reverse 3 | from django.http import HttpResponseNotAllowed, HttpResponseForbidden, \ 4 | HttpResponseBadRequest 5 | from django.test import Client, TestCase 6 | from reddit.models import Comment, Submission, Vote 7 | from django.contrib.auth.models import User 8 | from users.models import RedditUser 9 | 10 | class TestVotingOnItems(TestCase): 11 | def setUp(self): 12 | self.c = Client() 13 | self.credentials = { 14 | 'username': 'voteusername', 15 | 'password': 'password' 16 | } 17 | 18 | author = RedditUser.objects.create( 19 | user=User.objects.create_user( 20 | **self.credentials 21 | ) 22 | ) 23 | 24 | submission = Submission.objects.create( 25 | author=author, 26 | author_name=author.user.username, 27 | title="vote testing" 28 | ) 29 | 30 | Comment.create(author=author, 31 | raw_comment="root comment", 32 | parent=submission).save() 33 | 34 | def test_post_only(self): 35 | r = self.c.get(reverse('vote')) 36 | self.assertIsInstance(r, HttpResponseNotAllowed) 37 | 38 | def test_logged_out(self): 39 | test_data = { 40 | 'what': 'submission', 41 | 'what_id': 1, 42 | 'vote_value': 1 43 | } 44 | 45 | r = self.c.post(reverse('vote'), data=test_data) 46 | self.assertIsInstance(r, HttpResponseForbidden) 47 | 48 | def test_invalid_vote_value(self): 49 | self.c.login(**self.credentials) 50 | test_data = { 51 | 'what': 'submission', 52 | 'what_id': 1, 53 | 'vote_value': '2' 54 | } 55 | r = self.c.post(reverse('vote'), data=test_data) 56 | self.assertIsInstance(r, HttpResponseBadRequest) 57 | 58 | def test_missing_arugmnets(self): 59 | self.c.login(**self.credentials) 60 | r = self.c.post(reverse('vote'), 61 | data={ 62 | 'what': 'submission', 63 | 'what_id': 1 64 | }) 65 | self.assertIsInstance(r, HttpResponseBadRequest) 66 | r = self.c.post(reverse('vote'), 67 | data={ 68 | 'what': 'submission', 69 | 'vote_value': '1' 70 | }) 71 | self.assertIsInstance(r, HttpResponseBadRequest) 72 | r = self.c.post(reverse('vote'), 73 | data={ 74 | 'what_id': '1', 75 | 'vote_value': '1' 76 | }) 77 | self.assertIsInstance(r, HttpResponseBadRequest) 78 | r = self.c.post(reverse('vote'), data={}) 79 | self.assertIsInstance(r, HttpResponseBadRequest) 80 | 81 | def test_invalid_vote_object_id(self): 82 | self.c.login(**self.credentials) 83 | for what_type in ['comment', 'submission']: 84 | test_data = { 85 | 'what': what_type, 86 | 'what_id': 9999, 87 | 'what_value': '1' 88 | } 89 | r = self.c.post(reverse('vote'), data=test_data) 90 | self.assertIsInstance(r, HttpResponseBadRequest) 91 | 92 | def test_submission_first_vote(self): 93 | submission = Submission.objects.filter(title="vote testing").first() 94 | self.assertIsNotNone(submission) 95 | self.c.login(**self.credentials) 96 | r = self.c.post(reverse('vote'), 97 | data={ 98 | 'what': 'submission', 99 | 'what_id': submission.id, 100 | 'vote_value': '1' 101 | }) 102 | self.assertEqual(r.status_code, 200) 103 | json_r = json.loads(r.content.decode("utf-8")) 104 | self.assertIsNone(json_r['error']) 105 | self.assertEqual(json_r['voteDiff'], 1) 106 | submission = Submission.objects.filter(title="vote testing").first() 107 | self.assertEqual(submission.score, 1) 108 | 109 | def test_submission_vote_cancel_or_reverse(self): 110 | submission = Submission.objects.filter(title="vote testing").first() 111 | user = RedditUser.objects.get( 112 | user=User.objects.get(username=self.credentials['username'])) 113 | self.assertIsNotNone(submission) 114 | self.assertIsNotNone(user) 115 | Vote.create(user=user, vote_object=submission, vote_value=1).save() 116 | 117 | self.c.login(**self.credentials) 118 | r = self.c.post(reverse('vote'), 119 | data={ 120 | 'what': 'submission', 121 | 'what_id': submission.id, 122 | 'vote_value': '1' 123 | }) 124 | self.assertEqual(r.status_code, 200) 125 | json_r = json.loads(r.content.decode("utf-8")) 126 | self.assertIsNone(json_r['error']) 127 | self.assertEqual(json_r['voteDiff'], -1) 128 | 129 | vote = Vote.objects.get(vote_object_type=submission.get_content_type(), 130 | vote_object_id=submission.id, 131 | user=user) 132 | vote.value = 1 133 | vote.save() 134 | 135 | self.c.login(**self.credentials) 136 | r = self.c.post(reverse('vote'), 137 | data={ 138 | 'what': 'submission', 139 | 'what_id': submission.id, 140 | 'vote_value': '-1' 141 | }) 142 | self.assertEqual(r.status_code, 200) 143 | json_r = json.loads(r.content.decode("utf-8")) 144 | self.assertIsNone(json_r['error']) 145 | self.assertEqual(json_r['voteDiff'], -2) 146 | 147 | def test_comment_first_vote(self): 148 | submission = Submission.objects.filter(title="vote testing").first() 149 | self.assertIsNotNone(submission) 150 | comment = Comment.objects.filter(submission=submission).first() 151 | self.assertIsNotNone(comment) 152 | self.c.login(**self.credentials) 153 | r = self.c.post(reverse('vote'), 154 | data={ 155 | 'what': 'comment', 156 | 'what_id': comment.id, 157 | 'vote_value': '1' 158 | }) 159 | self.assertEqual(r.status_code, 200) 160 | json_r = json.loads(r.content.decode("utf-8")) 161 | self.assertIsNone(json_r['error']) 162 | self.assertEqual(json_r['voteDiff'], 1) 163 | scomment = Comment.objects.filter(submission=submission).first() 164 | self.assertEqual(scomment.score, 1) 165 | 166 | def test_comment_vote_cancel_or_reverse(self): 167 | submission = Submission.objects.filter(title="vote testing").first() 168 | user = RedditUser.objects.get( 169 | user=User.objects.get(username=self.credentials['username'])) 170 | self.assertIsNotNone(submission) 171 | self.assertIsNotNone(user) 172 | comment = Comment.objects.filter(submission=submission).first() 173 | self.assertIsNotNone(comment) 174 | Vote.create(user=user, vote_object=comment, vote_value=1).save() 175 | 176 | self.c.login(**self.credentials) 177 | r = self.c.post(reverse('vote'), 178 | data={ 179 | 'what': 'comment', 180 | 'what_id': comment.id, 181 | 'vote_value': '1' 182 | }) 183 | self.assertEqual(r.status_code, 200) 184 | json_r = json.loads(r.content.decode("utf-8")) 185 | self.assertIsNone(json_r['error']) 186 | self.assertEqual(json_r['voteDiff'], -1) 187 | 188 | vote = Vote.objects.get(vote_object_type=comment.get_content_type(), 189 | vote_object_id=comment.id, 190 | user=user) 191 | vote.value = 1 192 | vote.save() 193 | 194 | self.c.login(**self.credentials) 195 | r = self.c.post(reverse('vote'), 196 | data={ 197 | 'what': 'comment', 198 | 'what_id': comment.id, 199 | 'vote_value': '-1' 200 | }) 201 | self.assertEqual(r.status_code, 200) 202 | json_r = json.loads(r.content.decode("utf-8")) 203 | self.assertIsNone(json_r['error']) 204 | self.assertEqual(json_r['voteDiff'], -2) 205 | -------------------------------------------------------------------------------- /reddit/urls.py: -------------------------------------------------------------------------------- 1 | """django_reddit URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import url 17 | 18 | from . import views 19 | 20 | urlpatterns = [ 21 | url(r'^$', views.frontpage, name="frontpage"), 22 | url(r'^comments/(?P[0-9]+)$', views.comments, name="thread"), 23 | url(r'^submit/$', views.submit, name="submit"), 24 | url(r'^post/comment/$', views.post_comment, name="post_comment"), 25 | url(r'^vote/$', views.vote, name="vote"), 26 | 27 | ] 28 | -------------------------------------------------------------------------------- /reddit/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /reddit/utils/helpers.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseNotAllowed 2 | 3 | def post_only(func):# pragma: no cover 4 | def decorated(request, *args, **kwargs): 5 | if request.method != 'POST': 6 | return HttpResponseNotAllowed(['GET']) 7 | return func(request, *args, **kwargs) 8 | return decorated 9 | 10 | def get_only(func):# pragma: no cover 11 | def decorated(request, *args, **kwargs): 12 | if request.method != 'GET': 13 | return HttpResponseNotAllowed(['POST']) 14 | return func(request, *args, **kwargs) 15 | return decorated 16 | -------------------------------------------------------------------------------- /reddit/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import login_required 3 | from django.contrib.auth.models import User 4 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 5 | from django.http import JsonResponse, HttpResponseBadRequest, Http404, \ 6 | HttpResponseForbidden 7 | from django.shortcuts import render, redirect, get_object_or_404 8 | from django.template.defaulttags import register 9 | 10 | from reddit.forms import SubmissionForm 11 | from reddit.models import Submission, Comment, Vote 12 | from reddit.utils.helpers import post_only 13 | from users.models import RedditUser 14 | 15 | 16 | @register.filter 17 | def get_item(dictionary, key): # pragma: no cover 18 | """ 19 | Needed because there's no built in .get in django templates 20 | when working with dictionaries. 21 | 22 | :param dictionary: python dictionary 23 | :param key: valid dictionary key type 24 | :return: value of that key or None 25 | """ 26 | return dictionary.get(key) 27 | 28 | 29 | def frontpage(request): 30 | """ 31 | Serves frontpage and all additional submission listings 32 | with maximum of 25 submissions per page. 33 | """ 34 | # TODO: Serve user votes on submissions too. 35 | 36 | all_submissions = Submission.objects.order_by('-score').all() 37 | paginator = Paginator(all_submissions, 25) 38 | 39 | page = request.GET.get('page', 1) 40 | try: 41 | submissions = paginator.page(page) 42 | except PageNotAnInteger: 43 | raise Http404 44 | except EmptyPage: 45 | submissions = paginator.page(paginator.num_pages) 46 | 47 | submission_votes = {} 48 | 49 | if request.user.is_authenticated(): 50 | for submission in submissions: 51 | try: 52 | vote = Vote.objects.get( 53 | vote_object_type=submission.get_content_type(), 54 | vote_object_id=submission.id, 55 | user=RedditUser.objects.get(user=request.user)) 56 | submission_votes[submission.id] = vote.value 57 | except Vote.DoesNotExist: 58 | pass 59 | 60 | return render(request, 'public/frontpage.html', {'submissions' : submissions, 61 | 'submission_votes': submission_votes}) 62 | 63 | 64 | def comments(request, thread_id=None): 65 | """ 66 | Handles comment view when user opens the thread. 67 | On top of serving all comments in the thread it will 68 | also return all votes user made in that thread 69 | so that we can easily update comments in template 70 | and display via css whether user voted or not. 71 | 72 | :param thread_id: Thread ID as it's stored in database 73 | :type thread_id: int 74 | """ 75 | 76 | this_submission = get_object_or_404(Submission, id=thread_id) 77 | 78 | thread_comments = Comment.objects.filter(submission=this_submission) 79 | 80 | if request.user.is_authenticated(): 81 | try: 82 | reddit_user = RedditUser.objects.get(user=request.user) 83 | except RedditUser.DoesNotExist: 84 | reddit_user = None 85 | else: 86 | reddit_user = None 87 | 88 | sub_vote_value = None 89 | comment_votes = {} 90 | 91 | if reddit_user: 92 | 93 | try: 94 | vote = Vote.objects.get( 95 | vote_object_type=this_submission.get_content_type(), 96 | vote_object_id=this_submission.id, 97 | user=reddit_user) 98 | sub_vote_value = vote.value 99 | except Vote.DoesNotExist: 100 | pass 101 | 102 | try: 103 | user_thread_votes = Vote.objects.filter(user=reddit_user, 104 | submission=this_submission) 105 | 106 | for vote in user_thread_votes: 107 | comment_votes[vote.vote_object.id] = vote.value 108 | except: 109 | pass 110 | 111 | return render(request, 'public/comments.html', 112 | {'submission' : this_submission, 113 | 'comments' : thread_comments, 114 | 'comment_votes': comment_votes, 115 | 'sub_vote' : sub_vote_value}) 116 | 117 | 118 | @post_only 119 | def post_comment(request): 120 | if not request.user.is_authenticated(): 121 | return JsonResponse({'msg': "You need to log in to post new comments."}) 122 | 123 | parent_type = request.POST.get('parentType', None) 124 | parent_id = request.POST.get('parentId', None) 125 | raw_comment = request.POST.get('commentContent', None) 126 | 127 | if not all([parent_id, parent_type]) or \ 128 | parent_type not in ['comment', 'submission'] or \ 129 | not parent_id.isdigit(): 130 | return HttpResponseBadRequest() 131 | 132 | if not raw_comment: 133 | return JsonResponse({'msg': "You have to write something."}) 134 | author = RedditUser.objects.get(user=request.user) 135 | parent_object = None 136 | try: # try and get comment or submission we're voting on 137 | if parent_type == 'comment': 138 | parent_object = Comment.objects.get(id=parent_id) 139 | elif parent_type == 'submission': 140 | parent_object = Submission.objects.get(id=parent_id) 141 | 142 | except (Comment.DoesNotExist, Submission.DoesNotExist): 143 | return HttpResponseBadRequest() 144 | 145 | comment = Comment.create(author=author, 146 | raw_comment=raw_comment, 147 | parent=parent_object) 148 | 149 | comment.save() 150 | return JsonResponse({'msg': "Your comment has been posted."}) 151 | 152 | 153 | @post_only 154 | def vote(request): 155 | # The type of object we're voting on, can be 'submission' or 'comment' 156 | vote_object_type = request.POST.get('what', None) 157 | 158 | # The ID of that object as it's stored in the database, positive int 159 | vote_object_id = request.POST.get('what_id', None) 160 | 161 | # The value of the vote we're writing to that object, -1 or 1 162 | # Passing the same value twice will cancel the vote i.e. set it to 0 163 | new_vote_value = request.POST.get('vote_value', None) 164 | 165 | # By how much we'll change the score, used to modify score on the fly 166 | # client side by the javascript instead of waiting for a refresh. 167 | vote_diff = 0 168 | 169 | if not request.user.is_authenticated(): 170 | return HttpResponseForbidden() 171 | else: 172 | user = RedditUser.objects.get(user=request.user) 173 | 174 | try: # If the vote value isn't an integer that's equal to -1 or 1 175 | # the request is bad and we can not continue. 176 | new_vote_value = int(new_vote_value) 177 | 178 | if new_vote_value not in [-1, 1]: 179 | raise ValueError("Wrong value for the vote!") 180 | 181 | except (ValueError, TypeError): 182 | return HttpResponseBadRequest() 183 | 184 | # if one of the objects is None, 0 or some other bool(value) == False value 185 | # or if the object type isn't 'comment' or 'submission' it's a bad request 186 | if not all([vote_object_type, vote_object_id, new_vote_value]) or \ 187 | vote_object_type not in ['comment', 'submission']: 188 | return HttpResponseBadRequest() 189 | 190 | # Try and get the actual object we're voting on. 191 | try: 192 | if vote_object_type == "comment": 193 | vote_object = Comment.objects.get(id=vote_object_id) 194 | 195 | elif vote_object_type == "submission": 196 | vote_object = Submission.objects.get(id=vote_object_id) 197 | else: 198 | return HttpResponseBadRequest() # should never happen 199 | 200 | except (Comment.DoesNotExist, Submission.DoesNotExist): 201 | return HttpResponseBadRequest() 202 | 203 | # Try and get the existing vote for this object, if it exists. 204 | try: 205 | vote = Vote.objects.get(vote_object_type=vote_object.get_content_type(), 206 | vote_object_id=vote_object.id, 207 | user=user) 208 | 209 | except Vote.DoesNotExist: 210 | # Create a new vote and that's it. 211 | vote = Vote.create(user=user, 212 | vote_object=vote_object, 213 | vote_value=new_vote_value) 214 | vote.save() 215 | vote_diff = new_vote_value 216 | return JsonResponse({'error' : None, 217 | 'voteDiff': vote_diff}) 218 | 219 | # User already voted on this item, this means the vote is either 220 | # being canceled (same value) or changed (different new_vote_value) 221 | if vote.value == new_vote_value: 222 | # canceling vote 223 | vote_diff = vote.cancel_vote() 224 | if not vote_diff: 225 | return HttpResponseBadRequest( 226 | 'Something went wrong while canceling the vote') 227 | else: 228 | # changing vote 229 | vote_diff = vote.change_vote(new_vote_value) 230 | if not vote_diff: 231 | return HttpResponseBadRequest( 232 | 'Wrong values for old/new vote combination') 233 | 234 | return JsonResponse({'error' : None, 235 | 'voteDiff': vote_diff}) 236 | 237 | 238 | @login_required 239 | def submit(request): 240 | """ 241 | Handles new submission.. submission. 242 | """ 243 | submission_form = SubmissionForm() 244 | 245 | if request.method == 'POST': 246 | submission_form = SubmissionForm(request.POST) 247 | if submission_form.is_valid(): 248 | submission = submission_form.save(commit=False) 249 | submission.generate_html() 250 | user = User.objects.get(username=request.user) 251 | redditUser = RedditUser.objects.get(user=user) 252 | submission.author = redditUser 253 | submission.author_name = user.username 254 | submission.save() 255 | messages.success(request, 'Submission created') 256 | return redirect('/comments/{}'.format(submission.id)) 257 | 258 | return render(request, 'public/submit.html', {'form': submission_form}) 259 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/production.txt 2 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.1 2 | django-environ==0.4.0 3 | django-secure==1.0.1 4 | django-widget-tweaks==1.4.1 5 | django-mptt==0.8.0 6 | mistune==0.7.1 7 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | # Local development dependencies 2 | -r base.txt 3 | django-debug-toolbar==1.4 4 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # Production dependencies 2 | -r base.txt 3 | gunicorn==19.4.1 4 | psycopg2==2.6.1 5 | -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.3.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0, 0)}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-genderless:before,.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"} -------------------------------------------------------------------------------- /static/css/reddit.css: -------------------------------------------------------------------------------- 1 | 2 | .title { 3 | color: blue; 4 | margin: 0 .4em 1px 0; 5 | } 6 | 7 | .domain, .domain a, .tagline, .buttons a { 8 | margin: 0; 9 | font-size: small; 10 | color: #888; 11 | } 12 | 13 | .buttons a:hover, .tagline a:hover, #undertaf a:hover { 14 | text-decoration: underline; 15 | } 16 | 17 | .buttons { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .buttons li { 23 | display: inline-block; 24 | padding-right: 4px; 25 | font-weight: bold; 26 | } 27 | 28 | /* remove underlines in links*/ 29 | a:link { 30 | text-decoration: none; 31 | } 32 | 33 | a:visited { 34 | text-decoration: none; 35 | } 36 | 37 | table a:not(.btn) { 38 | text-decoration: none; 39 | } 40 | 41 | html, 42 | body { 43 | height: 100%; 44 | /* The html and body elements cannot have any padding or margin. */ 45 | } 46 | 47 | code { 48 | font-size: 80%; 49 | } 50 | 51 | /* Sign up/Sign in*/ 52 | .form-signin { 53 | max-width: 330px; 54 | padding: 15px; 55 | margin: 0 auto; 56 | } 57 | 58 | .form-signin .form-signin-heading, 59 | .form-signin .checkbox { 60 | margin-bottom: 10px; 61 | } 62 | 63 | .form-signin .checkbox { 64 | font-weight: normal; 65 | } 66 | 67 | .form-signin .form-control { 68 | position: relative; 69 | height: auto; 70 | -webkit-box-sizing: border-box; 71 | -moz-box-sizing: border-box; 72 | box-sizing: border-box; 73 | padding: 10px; 74 | font-size: 16px; 75 | } 76 | 77 | .form-signin .form-control:focus { 78 | z-index: 2; 79 | } 80 | 81 | .form-signin input[type="email"] { 82 | margin-bottom: -1px; 83 | border-bottom-right-radius: 0; 84 | border-bottom-left-radius: 0; 85 | } 86 | 87 | .form-signin input[type="password"] { 88 | margin-bottom: 10px; 89 | border-top-left-radius: 0; 90 | border-top-right-radius: 0; 91 | } 92 | 93 | body { 94 | /* reduces size between upvote/score/downvote elements*/ 95 | line-height: 20px; 96 | } 97 | 98 | /* upvote/downvote */ 99 | .fa-chevron-up:hover { 100 | color: orangered; 101 | cursor: pointer; 102 | } 103 | 104 | .upvoted{ 105 | color: orangered; 106 | } 107 | 108 | .downvoted{ 109 | color: #5f99cf; 110 | } 111 | 112 | .fa-chevron-down:hover { 113 | color: #5f99cf; 114 | cursor: pointer; 115 | } 116 | 117 | .score:hover { 118 | cursor: default; 119 | 120 | } 121 | 122 | .vote { 123 | width:50px; 124 | font-size:14pt; 125 | text-align:center; 126 | /*line-height: 15px;*/ 127 | } 128 | 129 | .score { 130 | color:#737373; 131 | } 132 | 133 | .thread-info{ 134 | margin: 0; 135 | } 136 | 137 | .thread-title{ 138 | font-size: 20px; 139 | } 140 | 141 | .info-container{ 142 | padding-top: 5px; 143 | } 144 | 145 | .comment-group{ 146 | width: 50%; 147 | } 148 | 149 | .comment-media{ 150 | padding-right: 5px; 151 | } 152 | 153 | .comment-votes{ 154 | width: 20px; 155 | } 156 | 157 | p { 158 | margin:0; 159 | } 160 | 161 | #footer { 162 | height: 60px; 163 | background-color: #f5f5f5; 164 | margin-top:50px; 165 | padding-top:20px; 166 | padding-bottom:20px; 167 | } 168 | 169 | .profile 170 | { 171 | min-height: 275px; 172 | display: inline-block; 173 | } 174 | 175 | .img-responsive{ 176 | display: inline; 177 | } -------------------------------------------------------------------------------- /static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/img/sprite-reddit.EMWQffWtZwo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/static/img/sprite-reddit.EMWQffWtZwo.png -------------------------------------------------------------------------------- /static/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /static/js/reddit.js: -------------------------------------------------------------------------------- 1 | function vote(voteButton) { 2 | var csrftoken = getCookie('csrftoken'); 3 | 4 | $.ajaxSetup({ 5 | beforeSend: function (xhr, settings) { 6 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 7 | xhr.setRequestHeader("X-CSRFToken", csrftoken); 8 | } 9 | } 10 | }); 11 | var $voteDiv = $(voteButton).parent().parent(); 12 | var $data = $voteDiv.data(); 13 | var direction_name = $(voteButton).attr('title'); 14 | var vote_value = null; 15 | if (direction_name == "upvote") { 16 | vote_value = 1; 17 | } else if (direction_name == "downvote") { 18 | vote_value = -1; 19 | } else { 20 | return; 21 | } 22 | 23 | var doPost = $.post('/vote/', { 24 | what: $data.whatType, 25 | what_id: $data.whatId, 26 | vote_value: vote_value 27 | }); 28 | 29 | doPost.done(function (response) { 30 | if (response.error == null) { 31 | var voteDiff = response.voteDiff; 32 | var $score = null; 33 | var $upvoteArrow = null; 34 | var $downArrow = null; 35 | if ($data.whatType == 'submission') { 36 | $score = $voteDiv.find("div.score"); 37 | $upvoteArrow = $voteDiv.children("div").children('i.fa.fa-chevron-up'); 38 | $downArrow = $voteDiv.children("div").children('i.fa.fa-chevron-down'); 39 | } else if ($data.whatType == 'comment') { 40 | var $medaiDiv = $voteDiv.parent().parent(); 41 | var $votes = $medaiDiv.children('div.media-left').children('div.vote').children('div'); 42 | $upvoteArrow = $votes.children('i.fa.fa-chevron-up'); 43 | $downArrow = $votes.children('i.fa.fa-chevron-down'); 44 | $score = $medaiDiv.find('div.media-body:first').find("a.score:first"); 45 | 46 | } 47 | 48 | // update vote elements 49 | 50 | if (vote_value == -1) { 51 | if ($upvoteArrow.hasClass("upvoted")) { // remove upvote, if any. 52 | $upvoteArrow.removeClass("upvoted") 53 | } 54 | if ($downArrow.hasClass("downvoted")) { // Canceled downvote 55 | $downArrow.removeClass("downvoted") 56 | } else { // new downvote 57 | $downArrow.addClass("downvoted") 58 | } 59 | } else if (vote_value == 1) { // remove downvote 60 | if ($downArrow.hasClass("downvoted")) { 61 | $downArrow.removeClass("downvoted") 62 | } 63 | 64 | if ($upvoteArrow.hasClass("upvoted")) { // if canceling upvote 65 | $upvoteArrow.removeClass("upvoted") 66 | } else { // adding new upvote 67 | $upvoteArrow.addClass("upvoted") 68 | } 69 | } 70 | 71 | // update score element 72 | var scoreInt = parseInt($score.text()); 73 | $score.text(scoreInt += voteDiff); 74 | } 75 | }); 76 | } 77 | 78 | function getCookie(name) { 79 | var cookieValue = null; 80 | if (document.cookie && document.cookie != '') { 81 | var cookies = document.cookie.split(';'); 82 | for (var i = 0; i < cookies.length; i++) { 83 | var cookie = jQuery.trim(cookies[i]); 84 | // Does this cookie string begin with the name we want? 85 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 86 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 87 | break; 88 | } 89 | } 90 | } 91 | return cookieValue; 92 | } 93 | 94 | function csrfSafeMethod(method) { 95 | // these HTTP methods do not require CSRF protection 96 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 97 | } 98 | 99 | function submitEvent(event, form) { 100 | event.preventDefault(); 101 | var $form = form; 102 | var data = $form.data(); 103 | url = $form.attr("action"); 104 | commentContent = $form.find("textarea#commentContent").val(); 105 | 106 | var csrftoken = getCookie('csrftoken'); 107 | 108 | $.ajaxSetup({ 109 | beforeSend: function (xhr, settings) { 110 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 111 | xhr.setRequestHeader("X-CSRFToken", csrftoken); 112 | } 113 | } 114 | }); 115 | 116 | var doPost = $.post(url, { 117 | parentType: data.parentType, 118 | parentId: data.parentId, 119 | commentContent: commentContent 120 | }); 121 | 122 | doPost.done(function (response) { 123 | var errorLabel = $form.find("span#postResponse"); 124 | if (response.msg) { 125 | errorLabel.text(response.msg); 126 | errorLabel.removeAttr('style'); 127 | } 128 | }); 129 | } 130 | 131 | $("#commentForm").submit(function (event) { 132 | submitEvent(event, $(this)); 133 | }); 134 | 135 | var newCommentForm = '
\ 138 |
\ 139 |
\ 140 | \ 141 |
\ 142 | \ 143 | \ 144 |
\ 145 |
\ 146 |
\ 147 |
\ 148 | \ 149 |
\ 150 |
\ 151 |
\ 152 |
'; 153 | 154 | 155 | $('a[name="replyButton"]').click(function () { 156 | var $mediaBody = $(this).parent().parent().parent(); 157 | if ($mediaBody.find('#commentForm').length == 0) { 158 | $mediaBody.parent().find(".reply-container:first").append(newCommentForm); 159 | var $form = $mediaBody.find('#commentForm'); 160 | $form.data('parent-id', $mediaBody.parent().data().parentId); 161 | $form.submit(function (event) { 162 | submitEvent(event, $(this)); 163 | }); 164 | } else { 165 | $commentForm = $mediaBody.find('#commentForm:first'); 166 | if ($commentForm.attr('style') == null) { 167 | $commentForm.css('display', 'none') 168 | } else { 169 | $commentForm.removeAttr('style') 170 | } 171 | } 172 | 173 | }); 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /staticfiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/staticfiles/.gitkeep -------------------------------------------------------------------------------- /submissions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/submissions/__init__.py -------------------------------------------------------------------------------- /submissions/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /submissions/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SubmissionsConfig(AppConfig): 5 | name = 'submissions' 6 | -------------------------------------------------------------------------------- /submissions/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/submissions/forms.py -------------------------------------------------------------------------------- /submissions/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/submissions/migrations/__init__.py -------------------------------------------------------------------------------- /submissions/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /submissions/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/submissions/tests/__init__.py -------------------------------------------------------------------------------- /submissions/urls.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/submissions/urls.py -------------------------------------------------------------------------------- /submissions/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /templates/__items/comment.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | {% load mptt_tags %} 3 | 4 | {% recursetree comments %} 5 |
6 |
7 |
10 | {% with vote_value=comment_votes|get_item:node.id %} 11 |
13 |
14 |
17 | {% endwith %} 18 |
19 |
20 |
23 |
{{ node.author_name }} 24 | {{ node.score }} points posted {{ node.timestamp|naturaltime }}
25 | {{ node.html_comment|safe }} 26 |
27 | 30 |
31 | {% if not node.is_leaf_node %} 32 | {{ children }} 33 | {% endif %} 34 |
35 |
36 | {% endrecursetree %} -------------------------------------------------------------------------------- /templates/__layout/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/__layout/navbar.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}Django Reddit{% endblock %} 12 | 13 | 14 | {# #} 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | {% block navbar %} 27 | {% include '__layout/navbar.html' %} 28 | {% endblock %} 29 | {% for message in messages %} 30 |
31 | 32 | {{ message }} 33 |
34 | {% endfor %} 35 | 36 |
37 | {% block content %} 38 | There's nothing here! 39 | {% endblock %} 40 |
41 | 42 | {% include '__layout/footer.html' %} 43 | 44 | 45 | 46 | 47 | {% block js %} 48 | {% endblock %} 49 | 50 | 51 | -------------------------------------------------------------------------------- /templates/private/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% csrf_token %} 7 |
8 | Edit your profile 9 |
10 |
11 |
12 | 13 | 14 |
15 | {{ form.first_name }} 16 | {% for error in form.first_name.errors %} 17 |

{{ error }}

18 | {% endfor %} 19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 | {{ form.last_name }} 29 | {% for error in form.last_name.errors %} 30 |

{{ error }}

31 | {% endfor %} 32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | 42 | 43 |
44 | {{ form.email }} 45 | {% for error in form.email.errors %} 46 |

{{ error }}

47 | {% endfor %} 48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 |
59 | 62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 | {{ form.about_text }} 73 | {% for error in form.about_text.errors %} 74 |

{{ error }}

75 | {% endfor %} 76 | Up to 500 characters of about me text that will be displayed on your proifle page. 77 |
78 |
79 | 80 |
81 |
82 |
83 | 84 | 85 |
86 | {{ form.homepage }} 87 | {% for error in form.homepage.errors %} 88 |

{{ error }}

89 | {% endfor %} 90 |
91 |
92 |
93 | 94 |
95 |
96 | 97 | 98 |
99 | https://github.com/ 100 | {{ form.github }} 101 | {% for error in form.github.errors %} 102 |

{{ error }}

103 | {% endfor %} 104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 | 112 |
113 | http://twitter.com/ 114 | {{ form.twitter }} 115 | {% for error in form.twitter.errors %} 116 |

{{ error }}

117 | {% endfor %} 118 |
119 |
120 |
121 | 122 |
123 | 124 |
125 |
126 | 127 | View your profile 128 |
129 |
130 | 131 |
132 |
133 | 134 | {% endblock %} -------------------------------------------------------------------------------- /templates/public/comments.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load humanize %} 3 | 4 | {% block content %} 5 | {# Submission block#} 6 | 7 |
8 |
9 |
12 |
14 |
15 |
{{ submission.score }}
16 |
19 |
20 |
21 |
22 |
23 | {{ submission.title }} 24 |
25 |
submitted {{ submission.timestamp|naturaltime }} by {{ submission.author_name }}
27 | {{ submission.text_html|safe }} 28 | 31 |
32 |
33 | 34 | 35 | {# New comment block#} 36 | 37 |
41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 | 59 | {% include '__items/comment.html' %} 60 | 61 | {% endblock %} -------------------------------------------------------------------------------- /templates/public/frontpage.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load humanize %} 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | {% for submission in submissions %} 9 | 10 | 25 | 37 | 38 | {% endfor %} 39 | 40 |
11 |
14 | {% with vote_value=submission_votes|get_item:submission.id %} 15 |
17 |
18 |
{{ submission.score }}
19 |
22 | {% endwith %} 23 |
24 |
26 | {{ submission.title }} 27 |
28 |
submitted {{ submission.timestamp|naturaltime }} by {{ submission.author_name }}
30 | 31 | 34 | 35 | 36 |
41 | 42 | 59 | 60 | {% endblock %} -------------------------------------------------------------------------------- /templates/public/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/public/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |
9 |
10 |

{% if profile.first_name %}{{ profile.first_name }}{% endif %} 11 | {% if profile.last_name %}{{ profile.last_name }} {% endif %} 12 | ({{ profile.user.username }}) 13 |

14 | 15 |

About:

16 | 17 |

18 | {% if profile.about_html %} 19 | {{ profile.about_html|safe }} 20 | {% else %} 21 |

User has not entered anything here

22 | {% endif %} 23 |

24 |
25 |
26 |
27 | {% if profile.display_picture %} 28 | 30 | {% else %} 31 | 33 | {% endif %} 34 |
35 | 36 |
37 | {% if profile.homepage %} 38 | 40 | 41 | 42 | {% endif %} 43 | 44 | {% if profile.twitter %} 45 | 47 | 48 | 49 | {% endif %} 50 | 51 | {% if profile.github %} 52 | 54 | 55 | 56 | {% endif %} 57 |
58 |
59 |
60 |
61 | 62 |

{{ profile.comment_karma }}

63 | 64 |

Comment karma

65 |
66 |
67 |

{{ profile.link_karma }}

68 | 69 |

Link karma

70 |
71 |
72 |
73 | {% if profile.user == user %} 74 | Edit 76 | {% else %} 77 | 78 | Message 79 | {% endif %} 80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | 88 | {% endblock %} -------------------------------------------------------------------------------- /templates/public/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/public/submit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 |
6 | New submission 7 | 8 | {% for field in form %} 9 | 10 |
11 | 12 | 13 |
14 | {% for error in field.errors %} 15 |

{{ error }}

16 | 17 | {% endfor %} 18 | {{ field }} 19 |
20 |
21 | {% endfor %} 22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 | {% endblock %} -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from reddit.admin import SubmissionInline 5 | from users.models import RedditUser 6 | 7 | 8 | class RedditUserAdmin(admin.ModelAdmin): 9 | inlines = [ 10 | SubmissionInline, 11 | ] 12 | 13 | admin.site.register(RedditUser, RedditUserAdmin) 14 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/users/forms.py -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-23 20:31 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='RedditUser', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('first_name', models.CharField(blank=True, default=None, max_length=35, null=True)), 24 | ('last_name', models.CharField(blank=True, default=None, max_length=35, null=True)), 25 | ('email', models.EmailField(blank=True, default=None, max_length=254, null=True)), 26 | ('about_text', models.TextField(blank=True, default=None, max_length=500, null=True)), 27 | ('about_html', models.TextField(blank=True, default=None, null=True)), 28 | ('gravatar_hash', models.CharField(blank=True, default=None, max_length=32, null=True)), 29 | ('display_picture', models.NullBooleanField(default=False)), 30 | ('homepage', models.URLField(blank=True, default=None, null=True)), 31 | ('twitter', models.CharField(blank=True, default=None, max_length=15, null=True)), 32 | ('github', models.CharField(blank=True, default=None, max_length=39, null=True)), 33 | ('comment_karma', models.IntegerField(default=0)), 34 | ('link_karma', models.IntegerField(default=0)), 35 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | import mistune 4 | from django.contrib.auth.models import User 5 | from django.db import models 6 | 7 | 8 | class RedditUser(models.Model): 9 | user = models.OneToOneField(User) 10 | first_name = models.CharField(max_length=35, null=True, default=None, 11 | blank=True) 12 | last_name = models.CharField(max_length=35, null=True, default=None, 13 | blank=True) 14 | email = models.EmailField(null=True, blank=True, default=None) 15 | about_text = models.TextField(blank=True, null=True, max_length=500, 16 | default=None) 17 | about_html = models.TextField(blank=True, null=True, default=None) 18 | gravatar_hash = models.CharField(max_length=32, null=True, blank=True, 19 | default=None) 20 | display_picture = models.NullBooleanField(default=False) 21 | homepage = models.URLField(null=True, blank=True, default=None) 22 | twitter = models.CharField(null=True, blank=True, max_length=15, 23 | default=None) 24 | github = models.CharField(null=True, blank=True, max_length=39, 25 | default=None) 26 | 27 | comment_karma = models.IntegerField(default=0) 28 | link_karma = models.IntegerField(default=0) 29 | 30 | def update_profile_data(self): 31 | self.about_html = mistune.markdown(self.about_text) 32 | if self.display_picture: 33 | self.gravatar_hash = md5(self.email.lower().encode('utf-8')).hexdigest() 34 | 35 | def __unicode__(self): 36 | return "".format(self.user.username) 37 | -------------------------------------------------------------------------------- /users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolak/django_reddit/9a89deb0e3a4028f3e96c806d7288c3163da2223/users/tests/__init__.py -------------------------------------------------------------------------------- /users/tests/test_login.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class TestloginPOST(TestCase): 7 | def setUp(self): 8 | self.c = Client() 9 | self.valid_data = {"username": "user", 10 | "password": "password"} 11 | self.invalid_data = {"username": "none", 12 | "password": "none"} 13 | User.objects.create_user(**self.valid_data) 14 | 15 | def test_valid_login(self): 16 | r = self.c.post(reverse('login'), data=self.valid_data, follow=True) 17 | self.assertRedirects(r, reverse('frontpage')) 18 | self.assertTrue(self.c.login(**self.valid_data)) 19 | 20 | def test_invalid_login(self): 21 | r = self.c.post(reverse('login'), data=self.invalid_data) 22 | self.assertEqual(r.status_code, 200) 23 | self.assertContains(r, "Wrong username or password.") 24 | self.assertFalse(self.c.login(**self.invalid_data)) 25 | 26 | def test_disabled_account(self): 27 | u = User.objects.get(username=self.valid_data['username']) 28 | u.is_active = False 29 | u.save() 30 | r = self.c.post(reverse('login'), data=self.valid_data) 31 | self.assertContains(r, "Account disabled", status_code=200) 32 | 33 | def test_already_logged_in(self): 34 | self.c.post(reverse('login'), data=self.valid_data) 35 | r = self.c.post(reverse('login'), data=self.valid_data) 36 | self.assertContains(r, 'You are already logged in.') 37 | 38 | def test_login_redirect(self): 39 | redirect_data = {'username': self.valid_data['username'], 40 | 'password': self.valid_data['password'], 41 | 'next': reverse('submit')} 42 | r = self.c.post(reverse('login'), data=redirect_data) 43 | self.assertRedirects(r, reverse('submit')) 44 | 45 | def test_malformed_request(self): 46 | r = self.c.post(reverse('login'), data={"a": "a", 1: "b"}) 47 | self.assertEqual(r.status_code, 400) 48 | -------------------------------------------------------------------------------- /users/tests/test_logout.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class Testlogout(TestCase): 7 | def setUp(self): 8 | self.c = Client() 9 | self.login_data = {'username': 'username', 10 | 'password': 'password'} 11 | User.objects.create_user(**self.login_data) 12 | 13 | def test_valid_logout(self): 14 | self.assertTrue(self.c.login(**self.login_data)) 15 | r = self.c.post(reverse('logout'), follow=True) 16 | self.assertRedirects(r, reverse('frontpage')) 17 | self.assertContains(r, 'Logged out!') 18 | 19 | def test_custom_logout_redirect(self): 20 | self.assertTrue(self.c.login(**self.login_data)) 21 | r = self.c.post(reverse('logout'), data={'current_page': reverse('login')}, follow=True) 22 | self.assertRedirects(r, reverse('login')) 23 | self.assertContains(r, 'Logged out!') 24 | 25 | def test_invalid_logout_request(self): 26 | r = self.c.post(reverse('logout'), follow=True) 27 | self.assertRedirects(r, reverse('frontpage')) 28 | self.assertTrue('Logged out!' not in r, 29 | msg="User that was not logged in told he logged out successfully") 30 | -------------------------------------------------------------------------------- /users/tests/test_registration.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | from reddit.forms import UserForm 4 | from django.contrib.auth.models import User 5 | from users.models import RedditUser 6 | 7 | 8 | class RegistrationFormTestCase(TestCase): 9 | def setUp(self): 10 | User.objects.create(username="user", password="password") 11 | 12 | def test_valid_form(self): 13 | test_data = {'username': 'username', 14 | 'password': 'password'} 15 | form = UserForm(data=test_data) 16 | self.assertTrue(form.is_valid()) 17 | 18 | def test_too_short_username(self): 19 | test_data = {'username': '_', 20 | 'password': 'password'} 21 | form = UserForm(data=test_data) 22 | self.assertEqual(form.errors['username'], [ 23 | "Ensure this value has at least 3 characters (it has 1)."]) 24 | self.assertFalse(form.is_valid()) 25 | 26 | def test_too_long_username(self): 27 | test_data = {'username': '1234567890123', 28 | 'password': 'password'} 29 | 30 | form = UserForm(data=test_data) 31 | self.assertEqual(form.errors['username'], [ 32 | "Ensure this value has at most 12 characters (it has 13)."]) 33 | self.assertFalse(form.is_valid()) 34 | 35 | def test_invalid_username(self): 36 | test_data = {'username': 'matt-ex', 37 | 'password': 'password'} 38 | form = UserForm(data=test_data) 39 | self.assertEqual(form.errors['username'], 40 | ["This value may contain only letters, " 41 | "numbers and _ characters."]) 42 | 43 | def test_invalid_password(self): 44 | test_data = {'username': 'username', 45 | 'password': '_'} 46 | form = UserForm(data=test_data) 47 | self.assertEqual(form.errors['password'], [ 48 | "Ensure this value has at least 4 characters (it has 1)."]) 49 | self.assertFalse(form.is_valid()) 50 | 51 | def test_existing_username(self): 52 | test_data = {'username': 'user', 53 | 'password': 'password'} 54 | 55 | form = UserForm(data=test_data) 56 | self.assertEqual(form.errors['username'], 57 | ["A user with that username already exists."]) 58 | self.assertFalse(form.is_valid()) 59 | 60 | def test_missing_username(self): 61 | test_data = {'password': 'password'} 62 | form = UserForm(data=test_data) 63 | self.assertEqual(form.errors['username'], ["This field is required."]) 64 | 65 | def test_missing_password(self): 66 | test_data = {'username': 'username'} 67 | form = UserForm(data=test_data) 68 | self.assertEqual(form.errors['password'], ["This field is required."]) 69 | 70 | 71 | class RegistrationPostTestCase(TestCase): 72 | def setUp(self): 73 | self.c = Client() 74 | 75 | def test_logged_in(self): 76 | User.objects.create_user(username='regtest', password='password') 77 | self.c.login(username='regtest', password='password') 78 | r = self.c.get(reverse('register')) 79 | self.assertContains(r, 'You are already registered and logged in.') 80 | self.assertContains(r, 'type="submit" disabled') 81 | 82 | def test_valid_data(self): 83 | test_data = {'username': 'username', 84 | 'password': 'password'} 85 | 86 | response = self.c.post(reverse('register'), data=test_data) 87 | self.assertRedirects(response, reverse('frontpage')) 88 | 89 | user = User.objects.filter(username=test_data['username']).first() 90 | self.assertIsNotNone(user, 91 | msg="User account was not created, but form submission did not fail") 92 | 93 | redditUser = RedditUser.objects.filter(user=user).first() 94 | self.assertIsNotNone(redditUser, 95 | msg="User created but not assigned to RedditUser model") 96 | 97 | self.assertTrue(self.c.login(**test_data), 98 | msg="User is unable to login.") 99 | self.assertEqual(RedditUser.objects.all().count(), 1) 100 | self.assertEqual(User.objects.all().count(), 1) 101 | 102 | def test_invalid_data(self): 103 | test_data = {'username': 'invalid_too_long_username', 104 | 'password': '_'} 105 | 106 | response = self.c.post(reverse('register'), data=test_data) 107 | self.assertEqual(response.status_code, 200, 108 | msg="Form submission failed, but a registration page was not returned again") 109 | self.assertIsNone( 110 | User.objects.filter(username=test_data['username']).first(), 111 | msg="Invalid user instance created") 112 | self.assertFalse(self.c.login(**test_data), 113 | msg="Invalid user data can be used to login") 114 | 115 | def test_malformed_post_request(self): 116 | response = self.c.post(reverse('register'), data={'a': "a", 1: "b"}) 117 | self.assertContains(response, 'This field is required.', count=2) 118 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^register/$', views.register, name="register"), 7 | url(r'^login/$', views.user_login, name="login"), 8 | url(r'^logout/$', views.user_logout, name="logout"), 9 | url(r'^user/(?P[0-9a-zA-Z_]*)$', views.user_profile, name="user_profile"), 10 | url(r'^profile/edit/$', views.edit_profile, name="edit_profile"), 11 | ] 12 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import logout, login, authenticate 3 | from django.contrib.auth.decorators import login_required 4 | from django.contrib.auth.models import User 5 | from django.http import HttpResponseBadRequest, Http404 6 | from django.shortcuts import render, redirect, get_object_or_404 7 | 8 | from reddit.forms import UserForm, ProfileForm 9 | from reddit.utils.helpers import post_only 10 | from users.models import RedditUser 11 | 12 | 13 | def user_profile(request, username): 14 | user = get_object_or_404(User, username=username) 15 | profile = RedditUser.objects.get(user=user) 16 | 17 | return render(request, 'public/profile.html', {'profile': profile}) 18 | 19 | 20 | @login_required 21 | def edit_profile(request): 22 | user = RedditUser.objects.get(user=request.user) 23 | 24 | if request.method == 'GET': 25 | profile_form = ProfileForm(instance=user) 26 | 27 | elif request.method == 'POST': 28 | profile_form = ProfileForm(request.POST, instance=user) 29 | if profile_form.is_valid(): 30 | profile = profile_form.save(commit=False) 31 | profile.update_profile_data() 32 | profile.save() 33 | messages.success(request, "Profile settings saved") 34 | else: 35 | raise Http404 36 | 37 | return render(request, 'private/edit_profile.html', {'form': profile_form}) 38 | 39 | 40 | def user_login(request): 41 | """ 42 | Pretty straighforward user authentication using password and username 43 | supplied in the POST request. 44 | """ 45 | 46 | if request.user.is_authenticated(): 47 | messages.warning(request, "You are already logged in.") 48 | return render(request, 'public/login.html') 49 | 50 | if request.method == "POST": 51 | username = request.POST.get('username') 52 | password = request.POST.get('password') 53 | if not username or not password: 54 | return HttpResponseBadRequest() 55 | 56 | user = authenticate(username=username, 57 | password=password) 58 | 59 | if user: 60 | if user.is_active: 61 | login(request, user) 62 | redirect_url = request.POST.get('next') or 'frontpage' 63 | return redirect(redirect_url) 64 | else: 65 | return render(request, 'public/login.html', 66 | {'login_error': "Account disabled"}) 67 | else: 68 | return render(request, 'public/login.html', 69 | {'login_error': "Wrong username or password."}) 70 | 71 | return render(request, 'public/login.html') 72 | 73 | 74 | @post_only 75 | def user_logout(request): 76 | """ 77 | Log out user if one is logged in and redirect them to frontpage. 78 | """ 79 | 80 | if request.user.is_authenticated(): 81 | redirect_page = request.POST.get('current_page', '/') 82 | logout(request) 83 | messages.success(request, 'Logged out!') 84 | return redirect(redirect_page) 85 | return redirect('frontpage') 86 | 87 | 88 | def register(request): 89 | """ 90 | Handles user registration using UserForm from forms.py 91 | Creates new User and new RedditUser models if appropriate data 92 | has been supplied. 93 | 94 | If account has been created user is redirected to login page. 95 | """ 96 | user_form = UserForm() 97 | if request.user.is_authenticated(): 98 | messages.warning(request, 99 | 'You are already registered and logged in.') 100 | return render(request, 'public/register.html', {'form': user_form}) 101 | 102 | if request.method == "POST": 103 | user_form = UserForm(request.POST) 104 | 105 | if user_form.is_valid(): 106 | user = user_form.save() 107 | user.set_password(user.password) 108 | user.save() 109 | reddit_user = RedditUser() 110 | reddit_user.user = user 111 | reddit_user.save() 112 | user = authenticate(username=request.POST['username'], 113 | password=request.POST['password']) 114 | login(request, user) 115 | return redirect('frontpage') 116 | 117 | return render(request, 'public/register.html', {'form': user_form}) 118 | --------------------------------------------------------------------------------