├── .buildpacks ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── __init__.py ├── app.json ├── bootstrap └── bootstrap.css ├── core ├── __init__.py ├── admin.py ├── context_processors.py ├── enrich.py ├── feed_managers.py ├── feed_settings.py ├── fixtures │ ├── initial_data.json │ └── items.json ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── after_deploy.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py ├── tests.py ├── urls.py ├── utils │ ├── __init__.py │ ├── admin.py │ ├── loading.py │ └── request.py ├── views.py └── wsgi.py ├── dev_requirements.txt ├── install.md ├── manage.py ├── media └── items │ ├── 011.jpg │ ├── 144568575661705981_4ade117f9005.jpeg │ ├── 164471358120532732_a3ae9498ad97.jpg │ ├── 167111313523218252_a6046175bb32.jpeg │ ├── 184290639033145973_71e492dacec6.jpeg │ ├── 1addc6292e97f5f796cf368df5d4b322.jpg │ ├── 261379381_d8fde25053fa.jpg │ ├── 272236897059737539_c4806e0b14c4.jpg │ ├── 272236897059737539_c4806e0b14c4_1.jpg │ ├── 275088534757448505_4f85768f71ec.jpg │ ├── 281485446934954481_c04cfc76c018.png │ ├── 293704645_b4c6dab8ed99.jpg │ ├── 389230669370236857_335b7c698687.jpg │ ├── 6fb3f77d22ddc23305fcd1a2fd8c4e80.jpg │ ├── 6fb3f77d22ddc23305fcd1a2fd8c4e80_1.jpg │ ├── 70602b9d4f965eee1d0345d99622798f.jpg │ ├── 73f98aae32e30a729bdf1031a78db65a.jpg │ ├── e2110e4545f1cc11043659fdd9c07b43.jpg │ └── e315a43813c5533b9f47754406019e24.jpg ├── requirements.txt ├── static ├── CACHE │ ├── css │ │ ├── 1155c6cf9112.css │ │ ├── 12764bc158f7.css │ │ ├── 60ba55091dbd.css │ │ ├── bace5aed07e9.css │ │ └── style.1155c6cf9112.css │ ├── js │ │ └── 5faa4a199196.js │ └── manifest.json ├── css │ ├── _feedly.scss │ ├── bootstrap │ │ ├── LICENSE │ │ ├── _alerts.scss │ │ ├── _badges.scss │ │ ├── _breadcrumbs.scss │ │ ├── _button-groups.scss │ │ ├── _buttons.scss │ │ ├── _carousel.scss │ │ ├── _close.scss │ │ ├── _code.scss │ │ ├── _component-animations.scss │ │ ├── _dropdowns.scss │ │ ├── _forms.scss │ │ ├── _glyphicons.scss │ │ ├── _grid.scss │ │ ├── _input-groups.scss │ │ ├── _jumbotron.scss │ │ ├── _labels.scss │ │ ├── _list-group.scss │ │ ├── _media.scss │ │ ├── _mixins.scss │ │ ├── _modals.scss │ │ ├── _navbar.scss │ │ ├── _navs.scss │ │ ├── _normalize.scss │ │ ├── _pager.scss │ │ ├── _pagination.scss │ │ ├── _panels.scss │ │ ├── _popovers.scss │ │ ├── _print.scss │ │ ├── _progress-bars.scss │ │ ├── _responsive-utilities.scss │ │ ├── _scaffolding.scss │ │ ├── _tables.scss │ │ ├── _theme.scss │ │ ├── _thumbnails.scss │ │ ├── _tooltip.scss │ │ ├── _type.scss │ │ ├── _utilities.scss │ │ ├── _variables.scss │ │ └── _wells.scss │ └── style.scss ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── js │ ├── app.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── getstream.js └── templates ├── account └── login.html ├── activity ├── aggregated │ ├── follow.html │ ├── notification_follow.html │ └── pin.html ├── follow.html └── pin.html └── core ├── _base.html ├── _item.html ├── _nav.html ├── _pin.html ├── _profile.html ├── aggregated_feed.html ├── feed.html ├── notification_feed.html ├── people.html ├── profile.html ├── robots.txt └── trending.html /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/heroku/heroku-buildpack-python.git 2 | https://github.com/heroku/heroku-buildpack-ruby.git -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | 6 | .vagrant 7 | .DS_Store 8 | 9 | pinterest_example/static/CACHE/ 10 | .sass-cache 11 | .coverage 12 | 13 | *.sqlite 14 | /static/admin 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem 'compass' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | chunky_png (1.2.8) 5 | compass (0.12.2) 6 | chunky_png (~> 1.2) 7 | fssm (>= 0.2.7) 8 | sass (~> 3.1) 9 | fssm (0.2.10) 10 | sass (3.2.12) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | compass 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017, Stream.io Inc, and individual contributors. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted 6 | provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of 9 | conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 12 | conditions and the following disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors may 16 | be used to endorse or promote products derived from this software without specific prior 17 | written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 20 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 21 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 26 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn core.wsgi -w 3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Stream Django Example App 2 | 3 | This example Django app shows you how you can easily build a site similar to Pinterest. 4 | 5 | The project is based on the [stream-django](https://github.com/GetStream/stream-django) integration for [Stream](https://getstream.io/). There is also a lower level [Python - Stream integration](https://github.com/getstream/stream-python) library which is suitable for all Python applications. 6 | 7 | You can sign up for a Stream account at https://getstream.io/get_started. 8 | 9 | If you're looking to self-host your feed solution we suggest the open source [Stream-Framework](https://github.com/tschellenbach/Stream-Framework), created by the Stream founders. 10 | 11 | ### Live demo 12 | 13 | Try the [live demo](http://exampledjango.getstream.io). 14 | 15 | ## Deploying the app 16 | 17 | ### Heroku 18 | 19 | The best way to understand and try out this application is via Heroku. You can deploy the app, for free, simply by clicking the following button: 20 | 21 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 22 | 23 | ### Local 24 | 25 | If you prefer to run the project locally, simply follow the steps in the [install.md](install.md) file. 26 | 27 | ## Tutorial 28 | 29 | So we want the pins, comments and follows to show up in the user's feed. We start by installing stream_django 30 | 31 | ```pip install stream_django``` 32 | 33 | add stream_django to your ```INSTALLED_APPS``` 34 | 35 | ``` 36 | INSTALLED_APPS = [ 37 | ... 38 | 'stream_django' 39 | ] 40 | ``` 41 | 42 | We indicate which models should be shared by using the Activity mixin. 43 | 44 | ``` 45 | class Pin(Activity, models.Model): 46 | ... 47 | ``` 48 | 49 | Now when users will add a pin it will automatically show up on the user feed. However to setup the newsfeed we also need 50 | to know who follows who. 51 | 52 | ``` 53 | feed_manager.follow_user(user_id, target_id) 54 | ``` 55 | 56 | This is all that's needed to setup the newsfeed. To retrieve the newsfeed we use the following code 57 | 58 | ``` 59 | @login_required 60 | def feed(request): 61 | ''' 62 | Items pinned by the people you follow 63 | ''' 64 | enricher = Enrich(request.user) 65 | context = RequestContext(request) 66 | feed = feed_manager.get_news_feeds(request.user.id)['timeline'] 67 | if request.POST.get('delete'): 68 | feed.delete() 69 | activities = feed.get(limit=25)['results'] 70 | context['activities'] = enricher.enrich_activities(activities) 71 | response = render_to_response('core/feed.html', context) 72 | return response 73 | ``` 74 | 75 | The last bit of work is making sure the templates render nicely. 76 | 77 | ``` 78 | {% for activity in activities %} 79 | {% render_activity activity %} 80 | {% endfor %} 81 | ``` 82 | 83 | This will render the templates in 84 | 85 | https://github.com/GetStream/Stream-Example-Py/tree/master/templates/activity 86 | 87 | ## Copyright and License Information 88 | 89 | Copyright (c) 2014-2017 Stream.io Inc, and individual contributors. All rights reserved. 90 | 91 | See the file "LICENSE" for information on the history of this software, terms & conditions for usage, and a DISCLAIMER OF ALL WARRANTIES. 92 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/__init__.py -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stream Example App", 3 | "description": "Pinterest esque example app using GetStream.io", 4 | "keywords": [ 5 | "getstream.io", 6 | "HTML5", 7 | "python" 8 | ], 9 | "website": "https://getstream.io/", 10 | "repository": "https://github.com/GetStream/stream-example-py", 11 | "logo": "https://dvqg2dogggmn6.cloudfront.net/images/stream_logo.svg", 12 | "success_url": "/", 13 | "env": { 14 | "BUILDPACK_URL": "git://github.com/heroku/heroku-buildpack-python.git", 15 | "DISABLE_COLLECTSTATIC": "1" 16 | }, 17 | "addons": [ 18 | "stream", "heroku-postgresql:hobby-dev", "rollbar" 19 | ], 20 | "scripts": { 21 | "postdeploy": "python manage.py after_deploy" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from core import models as pinterest_models 2 | from core.utils.admin import auto_register 3 | 4 | from django.contrib import admin 5 | 6 | 7 | class ItemAdmin(admin.ModelAdmin): 8 | list_display = ('user', 'image', 'source_url', 'message') 9 | list_editable = ('source_url', 'message') 10 | 11 | admin.site.register(pinterest_models.Item, ItemAdmin) 12 | 13 | auto_register(pinterest_models) 14 | -------------------------------------------------------------------------------- /core/context_processors.py: -------------------------------------------------------------------------------- 1 | from stream_django import feed_manager 2 | 3 | 4 | def user_feeds(request): 5 | context = {} 6 | if request.user.is_authenticated(): 7 | for feed in ['user', 'flat', 'aggregated', 'notification']: 8 | context[feed + '_feed'] = feed_manager.get_feed(feed, request.user.id) 9 | return context 10 | 11 | 12 | def unseen_notifications(request): 13 | context = {} 14 | if request.user.is_authenticated(): 15 | feed = feed_manager.get_feed('notification', request.user.id) 16 | context['unseen_notifications'] = feed.get().get('unseen', 0) 17 | context['unread_notifications'] = feed.get().get('unread', 0) 18 | return context 19 | -------------------------------------------------------------------------------- /core/enrich.py: -------------------------------------------------------------------------------- 1 | from stream_django.enrich import Enrich as BaseEnrich 2 | from core.models import Follow 3 | from core.models import Pin 4 | 5 | 6 | def did_i_pin_items(user, items): 7 | pinned_items_ids = user.pin_set.filter(item_id__in=items, deleted_at__isnull=True).values_list('item_id', flat=True) 8 | for item in items: 9 | item.pinned = item.id in pinned_items_ids 10 | 11 | 12 | def did_i_pin(user, pins): 13 | did_i_pin_items(user, [pin.item for pin in pins]) 14 | 15 | 16 | def do_i_follow_users(user, users): 17 | followed_user_ids = Follow.objects.filter(user_id=user.id, target__in=users, deleted_at__isnull=True).values_list('target_id', flat=True) 18 | for u in users: 19 | u.followed = u.id in followed_user_ids 20 | 21 | 22 | def do_i_follow(user, follows): 23 | do_i_follow_users(user, [f.target for f in follows]) 24 | 25 | 26 | class Enrich(BaseEnrich): 27 | 28 | def __init__(self, current_user, *args, **kwargs): 29 | super(Enrich, self).__init__(*args, **kwargs) 30 | self.current_user = current_user 31 | 32 | def fetch_pin_instances(self, pks): 33 | pins = Pin.objects.select_related(*Pin.activity_related_models()).in_bulk(pks) 34 | if self.current_user.is_authenticated(): 35 | did_i_pin(self.current_user, pins.values()) 36 | return pins 37 | 38 | def fetch_follow_instances(self, pks): 39 | follows = Follow.objects.select_related(*Follow.activity_related_models()).in_bulk(pks) 40 | if self.current_user.is_authenticated(): 41 | do_i_follow(self.current_user, follows.values()) 42 | return follows 43 | -------------------------------------------------------------------------------- /core/feed_managers.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/feed_managers.py -------------------------------------------------------------------------------- /core/feed_settings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/feed_settings.py -------------------------------------------------------------------------------- /core/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "core.item", 5 | "fields": { 6 | "created_at": null, 7 | "source_url": "google.com", 8 | "user": 1, 9 | "message": "beautiful flowers in the window", 10 | "pin_count": 0, 11 | "image": "items/011.jpg" 12 | } 13 | }, 14 | { 15 | "pk": 2, 16 | "model": "core.item", 17 | "fields": { 18 | "created_at": null, 19 | "source_url": "http://www.google.com/", 20 | "user": 1, 21 | "message": "", 22 | "pin_count": 0, 23 | "image": "items/261379381_d8fde25053fa.jpg" 24 | } 25 | }, 26 | { 27 | "pk": 3, 28 | "model": "core.item", 29 | "fields": { 30 | "created_at": null, 31 | "source_url": "http://www.google.com/", 32 | "user": 1, 33 | "message": "", 34 | "pin_count": 0, 35 | "image": "items/293704645_b4c6dab8ed99.jpg" 36 | } 37 | }, 38 | { 39 | "pk": 4, 40 | "model": "core.item", 41 | "fields": { 42 | "created_at": null, 43 | "source_url": "http://www.google.com/", 44 | "user": 1, 45 | "message": "", 46 | "pin_count": 0, 47 | "image": "items/144568575661705981_4ade117f9005.jpeg" 48 | } 49 | }, 50 | { 51 | "pk": 5, 52 | "model": "core.item", 53 | "fields": { 54 | "created_at": null, 55 | "source_url": "http://www.google.com/", 56 | "user": 1, 57 | "message": "", 58 | "pin_count": 0, 59 | "image": "items/164471358120532732_a3ae9498ad97.jpg" 60 | } 61 | }, 62 | { 63 | "pk": 6, 64 | "model": "core.item", 65 | "fields": { 66 | "created_at": null, 67 | "source_url": "http://www.google.com/", 68 | "user": 1, 69 | "message": "", 70 | "pin_count": 0, 71 | "image": "items/184290639033145973_71e492dacec6.jpeg" 72 | } 73 | }, 74 | { 75 | "pk": 7, 76 | "model": "core.item", 77 | "fields": { 78 | "created_at": null, 79 | "source_url": "http://www.google.com/", 80 | "user": 1, 81 | "message": "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings", 82 | "pin_count": 0, 83 | "image": "items/272236897059737539_c4806e0b14c4.jpg" 84 | } 85 | }, 86 | { 87 | "pk": 9, 88 | "model": "core.item", 89 | "fields": { 90 | "created_at": null, 91 | "source_url": "http://www.google.com/", 92 | "user": 1, 93 | "message": "\"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, qui", 94 | "pin_count": 0, 95 | "image": "items/275088534757448505_4f85768f71ec.jpg" 96 | } 97 | }, 98 | { 99 | "pk": 10, 100 | "model": "core.item", 101 | "fields": { 102 | "created_at": null, 103 | "source_url": "http://www.google.com/", 104 | "user": 1, 105 | "message": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like ", 106 | "pin_count": 0, 107 | "image": "items/281485446934954481_c04cfc76c018.png" 108 | } 109 | }, 110 | { 111 | "pk": 11, 112 | "model": "core.item", 113 | "fields": { 114 | "created_at": null, 115 | "source_url": "http://www.google.com/", 116 | "user": 1, 117 | "message": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries", 118 | "pin_count": 0, 119 | "image": "items/389230669370236857_335b7c698687.jpg" 120 | } 121 | }, 122 | { 123 | "pk": 12, 124 | "model": "core.item", 125 | "fields": { 126 | "created_at": null, 127 | "source_url": "http://www.google.com/", 128 | "user": 1, 129 | "message": "squirel power!", 130 | "pin_count": 0, 131 | "image": "items/1addc6292e97f5f796cf368df5d4b322.jpg" 132 | } 133 | }, 134 | { 135 | "pk": 14, 136 | "model": "core.item", 137 | "fields": { 138 | "created_at": null, 139 | "source_url": "http://www.google.com", 140 | "user": 1, 141 | "message": "designed amazingness", 142 | "pin_count": 0, 143 | "image": "items/6fb3f77d22ddc23305fcd1a2fd8c4e80_1.jpg" 144 | } 145 | }, 146 | { 147 | "pk": 15, 148 | "model": "core.item", 149 | "fields": { 150 | "created_at": null, 151 | "source_url": "http://www.google.com/", 152 | "user": 1, 153 | "message": "Outdoor", 154 | "pin_count": 0, 155 | "image": "items/73f98aae32e30a729bdf1031a78db65a.jpg" 156 | } 157 | }, 158 | { 159 | "pk": 16, 160 | "model": "core.item", 161 | "fields": { 162 | "created_at": null, 163 | "source_url": "http://www.google.com/", 164 | "user": 1, 165 | "message": "paris", 166 | "pin_count": 0, 167 | "image": "items/70602b9d4f965eee1d0345d99622798f.jpg" 168 | } 169 | }, 170 | { 171 | "pk": 17, 172 | "model": "core.item", 173 | "fields": { 174 | "created_at": null, 175 | "source_url": "http://www.google.com/", 176 | "user": 1, 177 | "message": "relax", 178 | "pin_count": 0, 179 | "image": "items/167111313523218252_a6046175bb32.jpeg" 180 | } 181 | }, 182 | { 183 | "pk": 18, 184 | "model": "core.item", 185 | "fields": { 186 | "created_at": null, 187 | "source_url": "http://www.google.com/", 188 | "user": 1, 189 | "message": "cuddle", 190 | "pin_count": 0, 191 | "image": "items/e315a43813c5533b9f47754406019e24.jpg" 192 | } 193 | }, 194 | { 195 | "pk": 19, 196 | "model": "core.item", 197 | "fields": { 198 | "created_at": null, 199 | "source_url": "http://www.google.com/", 200 | "user": 1, 201 | "message": "", 202 | "pin_count": 0, 203 | "image": "items/e2110e4545f1cc11043659fdd9c07b43.jpg" 204 | } 205 | }, 206 | { 207 | "pk": 1, 208 | "model": "auth.user", 209 | "fields": { 210 | "username": "admin", 211 | "first_name": "", 212 | "last_name": "", 213 | "is_active": true, 214 | "is_superuser": true, 215 | "is_staff": true, 216 | "last_login": "2014-10-09T16:24:26.313Z", 217 | "groups": [], 218 | "user_permissions": [], 219 | "password": "pbkdf2_sha256$10000$FT3tB3mdmURx$Fl/IAFKGuF877fhAlJR35hdMVzEHKv4wj0D2hXpbVTI=", 220 | "email": "admin@gmail.com", 221 | "date_joined": "2013-07-02T12:33:19Z" 222 | } 223 | }, 224 | { 225 | "pk": 2, 226 | "model": "auth.user", 227 | "fields": { 228 | "username": "bogus", 229 | "first_name": "", 230 | "last_name": "", 231 | "is_active": true, 232 | "is_superuser": true, 233 | "is_staff": true, 234 | "last_login": "2013-07-02T14:48:46.121Z", 235 | "groups": [], 236 | "user_permissions": [], 237 | "password": "pbkdf2_sha256$10000$Z478RyQJUIa0$t5C03JAdc5is01I80lrozwSzmK+RfLXAtS/jCjgrVKE=", 238 | "email": "bogus@gmail.com", 239 | "date_joined": "2013-07-02T14:48:46.121Z" 240 | } 241 | }, 242 | { 243 | "pk": 3, 244 | "model": "auth.user", 245 | "fields": { 246 | "username": "tbarbugli", 247 | "first_name": "Tommaso", 248 | "last_name": "Barbugli", 249 | "is_active": true, 250 | "is_superuser": false, 251 | "is_staff": false, 252 | "last_login": "2014-10-09T16:23:03.347Z", 253 | "groups": [], 254 | "user_permissions": [], 255 | "password": "!", 256 | "email": "tbarbugli@gmail.com", 257 | "date_joined": "2014-10-09T15:49:22.879Z" 258 | } 259 | } 260 | ] 261 | -------------------------------------------------------------------------------- /core/fixtures/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "core.item", 5 | "fields": { 6 | "pin_count": 0, 7 | "image": "items/011.jpg", 8 | "message": "beautiful flowers in the window", 9 | "user": 1, 10 | "source_url": "google.com" 11 | } 12 | }, 13 | { 14 | "pk": 2, 15 | "model": "core.item", 16 | "fields": { 17 | "pin_count": 0, 18 | "image": "items/261379381_d8fde25053fa.jpg", 19 | "message": "", 20 | "user": 1, 21 | "source_url": "http://www.google.com/" 22 | } 23 | }, 24 | { 25 | "pk": 3, 26 | "model": "core.item", 27 | "fields": { 28 | "pin_count": 0, 29 | "image": "items/293704645_b4c6dab8ed99.jpg", 30 | "message": "", 31 | "user": 1, 32 | "source_url": "http://www.google.com/" 33 | } 34 | }, 35 | { 36 | "pk": 4, 37 | "model": "core.item", 38 | "fields": { 39 | "pin_count": 0, 40 | "image": "items/144568575661705981_4ade117f9005.jpeg", 41 | "message": "", 42 | "user": 1, 43 | "source_url": "http://www.google.com/" 44 | } 45 | }, 46 | { 47 | "pk": 5, 48 | "model": "core.item", 49 | "fields": { 50 | "pin_count": 0, 51 | "image": "items/164471358120532732_a3ae9498ad97.jpg", 52 | "message": "", 53 | "user": 1, 54 | "source_url": "http://www.google.com/" 55 | } 56 | }, 57 | { 58 | "pk": 6, 59 | "model": "core.item", 60 | "fields": { 61 | "pin_count": 0, 62 | "image": "items/184290639033145973_71e492dacec6.jpeg", 63 | "message": "", 64 | "user": 1, 65 | "source_url": "http://www.google.com/" 66 | } 67 | }, 68 | { 69 | "pk": 7, 70 | "model": "core.item", 71 | "fields": { 72 | "pin_count": 0, 73 | "image": "items/272236897059737539_c4806e0b14c4.jpg", 74 | "message": "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings", 75 | "user": 1, 76 | "source_url": "http://www.google.com/" 77 | } 78 | }, 79 | { 80 | "pk": 9, 81 | "model": "core.item", 82 | "fields": { 83 | "pin_count": 0, 84 | "image": "items/275088534757448505_4f85768f71ec.jpg", 85 | "message": "\"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, qui", 86 | "user": 1, 87 | "source_url": "http://www.google.com/" 88 | } 89 | }, 90 | { 91 | "pk": 10, 92 | "model": "core.item", 93 | "fields": { 94 | "pin_count": 0, 95 | "image": "items/281485446934954481_c04cfc76c018.png", 96 | "message": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like ", 97 | "user": 1, 98 | "source_url": "http://www.google.com/" 99 | } 100 | }, 101 | { 102 | "pk": 11, 103 | "model": "core.item", 104 | "fields": { 105 | "pin_count": 0, 106 | "image": "items/389230669370236857_335b7c698687.jpg", 107 | "message": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries", 108 | "user": 1, 109 | "source_url": "http://www.google.com/" 110 | } 111 | }, 112 | { 113 | "pk": 12, 114 | "model": "core.item", 115 | "fields": { 116 | "pin_count": 0, 117 | "image": "items/1addc6292e97f5f796cf368df5d4b322.jpg", 118 | "message": "squirel power!", 119 | "user": 1, 120 | "source_url": "http://www.google.com/" 121 | } 122 | }, 123 | { 124 | "pk": 14, 125 | "model": "core.item", 126 | "fields": { 127 | "pin_count": 0, 128 | "image": "items/6fb3f77d22ddc23305fcd1a2fd8c4e80_1.jpg", 129 | "message": "designed amazingness", 130 | "user": 1, 131 | "source_url": "http://www.google.com" 132 | } 133 | }, 134 | { 135 | "pk": 15, 136 | "model": "core.item", 137 | "fields": { 138 | "pin_count": 0, 139 | "image": "items/73f98aae32e30a729bdf1031a78db65a.jpg", 140 | "message": "Outdoor", 141 | "user": 1, 142 | "source_url": "http://www.google.com/" 143 | } 144 | }, 145 | { 146 | "pk": 16, 147 | "model": "core.item", 148 | "fields": { 149 | "pin_count": 0, 150 | "image": "items/70602b9d4f965eee1d0345d99622798f.jpg", 151 | "message": "paris", 152 | "user": 1, 153 | "source_url": "http://www.google.com/" 154 | } 155 | }, 156 | { 157 | "pk": 17, 158 | "model": "core.item", 159 | "fields": { 160 | "pin_count": 0, 161 | "image": "items/167111313523218252_a6046175bb32.jpeg", 162 | "message": "relax", 163 | "user": 1, 164 | "source_url": "http://www.google.com/" 165 | } 166 | }, 167 | { 168 | "pk": 18, 169 | "model": "core.item", 170 | "fields": { 171 | "pin_count": 0, 172 | "image": "items/e315a43813c5533b9f47754406019e24.jpg", 173 | "message": "cuddle", 174 | "user": 1, 175 | "source_url": "http://www.google.com/" 176 | } 177 | }, 178 | { 179 | "pk": 19, 180 | "model": "core.item", 181 | "fields": { 182 | "pin_count": 0, 183 | "image": "items/e2110e4545f1cc11043659fdd9c07b43.jpg", 184 | "message": "", 185 | "user": 1, 186 | "source_url": "http://www.google.com/" 187 | } 188 | } 189 | ] 190 | -------------------------------------------------------------------------------- /core/forms.py: -------------------------------------------------------------------------------- 1 | from core.models import Follow 2 | from core.models import Pin 3 | from django import forms 4 | from datetime import datetime 5 | 6 | 7 | class PinForm(forms.Form): 8 | item = forms.IntegerField() 9 | influencer = forms.IntegerField() 10 | remove = forms.IntegerField(required=False) 11 | 12 | def __init__(self, user, *args, **kwargs): 13 | self.user = user 14 | super(PinForm, self).__init__(*args, **kwargs) 15 | 16 | def save(self, *args, **kwargs): 17 | item = self.cleaned_data['item'] 18 | influencer = self.cleaned_data['influencer'] 19 | remove = bool(int(self.cleaned_data.get('remove', 0) or 0)) 20 | if remove: 21 | now = datetime.now() 22 | pins = Pin.objects.filter( 23 | user=self.user, item=item) 24 | for pin in pins: 25 | pin.deleted_at = now 26 | pin.save() 27 | else: 28 | pin, created = Pin.objects.get_or_create(user=self.user, item_id=item, influencer_id=influencer) 29 | if not created and pin.deleted_at is not None: 30 | pin.deleted_at = None 31 | pin.save() 32 | 33 | 34 | class FollowForm(forms.Form): 35 | target = forms.IntegerField() 36 | remove = forms.IntegerField(required=False) 37 | 38 | def __init__(self, user, *args, **kwargs): 39 | self.user = user 40 | super(FollowForm, self).__init__(*args, **kwargs) 41 | 42 | def save(self): 43 | target = self.cleaned_data['target'] 44 | remove = bool(int(self.cleaned_data.get('remove', 0) or 0)) 45 | 46 | if remove: 47 | follows = Follow.objects.filter(user=self.user, target_id=target) 48 | now = datetime.now() 49 | for follow in follows: 50 | follow.deleted_at = now 51 | follow.save() 52 | else: 53 | follow, created = Follow.objects.get_or_create(user=self.user, target_id=target) 54 | if not created and follow.deleted_at is not None: 55 | follow.deleted_at = None 56 | follow.save() 57 | -------------------------------------------------------------------------------- /core/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/management/__init__.py -------------------------------------------------------------------------------- /core/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/management/commands/__init__.py -------------------------------------------------------------------------------- /core/management/commands/after_deploy.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from core.models import Follow 3 | from django.contrib.auth import get_user_model 4 | from django.core.management import call_command 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def handle(self, *args, **kwargs): 10 | call_command('migrate', interactive=False) 11 | call_command('loaddata', 'initial_data.json', interactive=False) 12 | admin = get_user_model().objects.get(username="admin") 13 | Follow.objects.create(user=admin, target=admin) 14 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.10 on 2016-10-10 14:27 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 | import stream_django.activity 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Follow', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)), 25 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 26 | ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_set', to=settings.AUTH_USER_MODEL)), 27 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_set', to=settings.AUTH_USER_MODEL)), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | bases=(stream_django.activity.Activity, models.Model), 33 | ), 34 | migrations.CreateModel( 35 | name='Item', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)), 39 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 40 | ('image', models.ImageField(upload_to=b'items')), 41 | ('source_url', models.TextField()), 42 | ('message', models.TextField(blank=True, null=True)), 43 | ('pin_count', models.IntegerField(default=0)), 44 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 45 | ], 46 | options={ 47 | 'abstract': False, 48 | }, 49 | ), 50 | migrations.CreateModel( 51 | name='Pin', 52 | fields=[ 53 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)), 55 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 56 | ('message', models.TextField(blank=True, null=True)), 57 | ('influencer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='influenced_pins', to=settings.AUTH_USER_MODEL)), 58 | ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Item')), 59 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 60 | ], 61 | options={ 62 | 'abstract': False, 63 | }, 64 | bases=(stream_django.activity.Activity, models.Model), 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.db.models.signals import post_delete, post_save 4 | from stream_django.activity import Activity 5 | from stream_django.feed_manager import feed_manager 6 | 7 | 8 | class BaseModel(models.Model): 9 | created_at = models.DateTimeField(blank=True, null=True, auto_now_add=True) 10 | deleted_at = models.DateTimeField(blank=True, null=True) 11 | 12 | class Meta: 13 | abstract = True 14 | 15 | 16 | class Item(BaseModel): 17 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 18 | image = models.ImageField(upload_to='items') 19 | source_url = models.TextField() 20 | message = models.TextField(blank=True, null=True) 21 | pin_count = models.IntegerField(default=0) 22 | 23 | 24 | class Pin(Activity, BaseModel): 25 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 26 | item = models.ForeignKey(Item) 27 | influencer = models.ForeignKey( 28 | settings.AUTH_USER_MODEL, related_name='influenced_pins') 29 | message = models.TextField(blank=True, null=True) 30 | 31 | @classmethod 32 | def activity_related_models(cls): 33 | return ['user', 'item'] 34 | 35 | @property 36 | def activity_object_attr(self): 37 | return self 38 | 39 | @property 40 | def extra_activity_data(self): 41 | return dict(item_id=self.item_id) 42 | 43 | 44 | def soft_delete(sender, instance, **kwargs): 45 | if instance.deleted_at is not None: 46 | feed_manager.activity_delete(sender, instance, **kwargs) 47 | 48 | 49 | post_save.connect(soft_delete, sender=Pin) 50 | 51 | 52 | class Follow(Activity, BaseModel): 53 | ''' 54 | A simple table mapping who a user is following. 55 | For example, if user is Kyle and Kyle is following Alex, 56 | the target would be Alex. 57 | ''' 58 | user = models.ForeignKey( 59 | settings.AUTH_USER_MODEL, related_name='following_set') 60 | target = models.ForeignKey( 61 | settings.AUTH_USER_MODEL, related_name='follower_set') 62 | 63 | @classmethod 64 | def activity_related_models(cls): 65 | return ['user', 'target'] 66 | 67 | @property 68 | def activity_object_attr(self): 69 | return self 70 | 71 | @property 72 | def activity_notify(self): 73 | target_feed = feed_manager.get_notification_feed(self.target_id) 74 | return [target_feed] 75 | 76 | 77 | def follow_change(sender, instance, created, **kwargs): 78 | if instance.deleted_at is None: 79 | feed_manager.follow_user(instance.user_id, instance.target_id) 80 | else: 81 | feed_manager.unfollow_user(instance.user_id, instance.target_id) 82 | 83 | 84 | def unfollow_feed(sender, instance, **kwargs): 85 | feed_manager.unfollow_user(instance.user_id, instance.target_id) 86 | 87 | 88 | post_save.connect(follow_change, sender=Follow) 89 | post_delete.connect(unfollow_feed, sender=Follow) 90 | 91 | -------------------------------------------------------------------------------- /core/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for pinterest_example project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import dj_database_url 13 | import os 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 16 | 17 | BASE_ROOT = os.path.abspath(os.path.join(os.path.split(__file__)[0], '..')) 18 | MEDIA_ROOT = os.path.join(BASE_ROOT, 'media/') 19 | STATIC_ROOT = os.path.join(BASE_ROOT, 'static/') 20 | DOCS_ROOT = os.path.join(BASE_ROOT, 'docs/') 21 | TEMPLATE_ROOT = os.path.join(BASE_ROOT, 'templates/') 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 24 | MEDIA_URL = '/media/' 25 | 26 | CACHES = { 27 | 'default': { 28 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 29 | } 30 | } 31 | 32 | # SECURITY WARNING: don't run with debug turned on in production! 33 | DEBUG = False 34 | 35 | 36 | COMPRESS_OFFLINE = False 37 | COMPRESS_ENABLED = True 38 | COMPRESS_PRECOMPILERS = ( 39 | ('text/x-sass', 'sass {infile} {outfile}'), 40 | ('text/x-scss', 'sass --scss {infile} {outfile}'), 41 | ) 42 | 43 | TEMPLATES = [{ 44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 45 | 'DIRS': [ 46 | TEMPLATE_ROOT 47 | ], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.contrib.auth.context_processors.auth', 52 | 'django.template.context_processors.debug', 53 | 'django.template.context_processors.i18n', 54 | 'django.template.context_processors.media', 55 | 'django.template.context_processors.static', 56 | 'django.template.context_processors.tz', 57 | 'django.template.context_processors.request', 58 | 'django.contrib.messages.context_processors.messages', 59 | 'stream_django.context_processors.stream', 60 | 'core.context_processors.user_feeds', 61 | 'core.context_processors.unseen_notifications', 62 | ], 63 | 'debug': DEBUG, 64 | }, 65 | },] 66 | 67 | # SECURITY WARNING: keep the secret key used in production secret! 68 | SECRET_KEY = 'ib_^kc#v536)v$x!h3*#xs6&l8&7#4cqi^rjhczu85l9txbz+w' 69 | 70 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 71 | ALLOWED_HOSTS = ['*'] 72 | 73 | # Application definition 74 | 75 | INSTALLED_APPS = ( 76 | 'django.contrib.admin', 77 | 'django.contrib.auth', 78 | 'django.contrib.contenttypes', 79 | 'django.contrib.sessions', 80 | 'django.contrib.messages', 81 | 'django.contrib.staticfiles', 82 | 'django.contrib.sites', 83 | 'stream_django', 84 | 'core', 85 | 'compressor', 86 | 'allauth', 87 | 'allauth.account', 88 | 'allauth.socialaccount', 89 | 'allauth.socialaccount.providers.github', 90 | 'sorl.thumbnail', 91 | ) 92 | 93 | STATICFILES_FINDERS = ( 94 | 'django.contrib.staticfiles.finders.FileSystemFinder', 95 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 96 | 'compressor.finders.CompressorFinder', 97 | ) 98 | 99 | MIDDLEWARE_CLASSES = ( 100 | 'django.contrib.sessions.middleware.SessionMiddleware', 101 | 'django.middleware.common.CommonMiddleware', 102 | 'django.middleware.csrf.CsrfViewMiddleware', 103 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 104 | 'django.contrib.messages.middleware.MessageMiddleware', 105 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 106 | ) 107 | 108 | ROLLBAR_ACCESS_TOKEN = os.environ.get('ROLLBAR_ACCESS_TOKEN') 109 | 110 | if ROLLBAR_ACCESS_TOKEN is not None: 111 | MIDDLEWARE_CLASSES += ('rollbar.contrib.django.middleware.RollbarNotifierMiddleware',) 112 | ROLLBAR = { 113 | 'access_token': ROLLBAR_ACCESS_TOKEN, 114 | 'environment': 'development' if DEBUG else 'production', 115 | 'branch': 'master', 116 | 'root': BASE_DIR, 117 | } 118 | 119 | ROOT_URLCONF = 'core.urls' 120 | 121 | WSGI_APPLICATION = 'core.wsgi.application' 122 | 123 | DATABASES = {'default':dj_database_url.config()} 124 | 125 | # Internationalization 126 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 127 | 128 | LANGUAGE_CODE = 'en-us' 129 | 130 | TIME_ZONE = 'UTC' 131 | 132 | USE_I18N = True 133 | 134 | USE_L10N = True 135 | 136 | USE_TZ = True 137 | 138 | 139 | # Static files (CSS, JavaScript, Images) 140 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 141 | 142 | STATIC_URL = '/static/' 143 | 144 | LOGGING = { 145 | 'version': 1, 146 | 'disable_existing_loggers': True, 147 | 'formatters': { 148 | 'verbose': { 149 | 'format': '%(levelname)s %(asctime)s %(message)s' 150 | #'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 151 | }, 152 | 'simple': { 153 | 'format': '%(levelname)s %(message)s' 154 | }, 155 | 'django.server': { 156 | '()': 'django.utils.log.ServerFormatter', 157 | 'format': '[%(server_time)s] %(message)s', 158 | }, 159 | }, 160 | 'handlers': { 161 | 'console': { 162 | 'level': 'DEBUG', 163 | 'class': 'logging.StreamHandler', 164 | 'formatter': 'simple' 165 | }, 166 | 'django.server': { 167 | 'level': 'INFO', 168 | 'class': 'logging.StreamHandler', 169 | 'formatter': 'django.server', 170 | }, 171 | }, 172 | 'loggers': { 173 | '': { 174 | 'handlers': [], 175 | 'level': 'WARNING', 176 | 'filters': [] 177 | }, 178 | 'django.server': { 179 | 'handlers': ['django.server'], 180 | 'level': 'INFO', 181 | 'propagate': False, 182 | }, 183 | } 184 | } 185 | 186 | USE_AUTH = bool(os.environ.get('USE_AUTH')) 187 | ACCOUNT_SIGNUP_PASSWORD_VERIFICATION = False 188 | ACCOUNT_AUTHENTICATION_METHOD = "username" 189 | ACCOUNT_EMAIL_VERIFICATION = "none" 190 | SITE_ID = int(os.environ.get('SITE_ID', 1)) 191 | LOGIN_REDIRECT_URL = '/' 192 | LOGOUT_REDIRECT_URL = '/' 193 | ACCOUNT_LOGOUT_ON_GET = True 194 | SOCIALACCOUNT_EMAIL_REQUIRED = False 195 | SOCIALACCOUNT_EMAIL_VERIFICATION = False 196 | 197 | # if you run on Heroku you don't need to set this 198 | STREAM_API_KEY = '' 199 | STREAM_API_SECRET = '' 200 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from django.contrib.auth import get_user_model 3 | from django.core.urlresolvers import reverse 4 | from django.test import Client 5 | from django.test.testcases import TestCase 6 | from core.models import Follow 7 | from mock import patch 8 | 9 | from stream_django.feed_manager import feed_manager 10 | 11 | 12 | class PinTestCase(TestCase): 13 | fixtures = ['initial_data'] 14 | 15 | def setUp(self): 16 | TestCase.setUp(self) 17 | from core.models import Item 18 | print(Item.objects.all().count()) 19 | self.client = Client() 20 | self.auth_client = Client() 21 | response = self.auth_client.login(username='admin', password='admin') 22 | self.assertTrue(response) 23 | self.admin = get_user_model().objects.get(username='admin') 24 | self.bogus = get_user_model().objects.get(username='bogus') 25 | 26 | def tearDown(self): 27 | feed_manager.disable_model_tracking() 28 | 29 | def test_pin(self): 30 | pin_url = reverse('pin') 31 | data = dict(message='test', board_name='My favourite things', item=5, 32 | influencer=1) 33 | 34 | feed_manager.enable_model_tracking() 35 | 36 | with patch('stream.feed.Feed.add_activity') as mocked: 37 | self.auth_client.post(pin_url, data) 38 | self.assertEqual(mocked.call_count, 1) 39 | 40 | def test_follow(self): 41 | with patch('stream.feed.Feed.follow') as mocked: 42 | Follow.objects.create(user=self.bogus, target=self.admin) 43 | print(mocked.mock_calls) 44 | # we have 2 feeds 45 | self.assertEqual(mocked.call_count, 2) 46 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | from django.contrib import admin 4 | from django.conf import settings 5 | import core.views as views 6 | from django.views.static import serve 7 | from django.views.generic import TemplateView 8 | 9 | admin.autodiscover() 10 | 11 | urlpatterns = [ 12 | url(r'^$', views.trending, name='trending'), 13 | # the three feed pages 14 | url(r'^feed/$', 15 | views.feed, name='feed'), 16 | url(r'^aggregated_feed/$', 17 | views.aggregated_feed, name='aggregated_feed'), 18 | url(r'^notification_feed/$', 19 | views.notification_feed, name='notification_feed'), 20 | # a page showing the users profile 21 | url(r'^profile/(?P[\w_-]+)/$', 22 | views.profile, name='profile'), 23 | # backends for follow and pin 24 | url(r'^pin/$', 25 | views.pin, name='pin'), 26 | url(r'^follow/$', 27 | views.follow, name='follow'), 28 | url(r'^people/$', 29 | views.people, name='people'), 30 | url(r'^auto_follow/$', 31 | views.auto_follow, name='auto_follow'), 32 | # the admin 33 | url(r'^admin/', include(admin.site.urls)), 34 | url(r'^accounts/', include('allauth.urls')), 35 | url(r'^auth/', include('django.contrib.auth.urls')), 36 | url(r'^robots.txt$', TemplateView.as_view(template_name='core/robots.txt', 37 | content_type="text/plain")) 38 | ] 39 | 40 | 41 | urlpatterns = [ 42 | url(r'^media/(?P.*)$', serve, 43 | {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), 44 | url(r'', include( 45 | 'django.contrib.staticfiles.urls')), 46 | ] + urlpatterns 47 | -------------------------------------------------------------------------------- /core/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/core/utils/__init__.py -------------------------------------------------------------------------------- /core/utils/admin.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import logging 4 | from django.contrib.admin.sites import AlreadyRegistered 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def auto_configure_admin(model): 9 | ''' 10 | Automatic configuration, let's make this smarter 11 | in the future 12 | ''' 13 | from django.contrib import admin 14 | from django.db import models 15 | fields = model._meta.fields 16 | field_names = [f.name for f in fields] 17 | list_display = [f for f in field_names if f not in ( 18 | 'updated_at', 'created_at')] 19 | fields_in_list_display = list_display[:7] 20 | list_editable_fields = [ 21 | f.name for f in fields if isinstance(f, models.IntegerField)] 22 | search_fields_list = [f.name for f in fields if isinstance( 23 | f, (models.TextField, models.CharField))] 24 | 25 | class StandardAdmin(admin.ModelAdmin): 26 | list_display = fields_in_list_display 27 | list_editable = list_editable_fields 28 | search_fields = search_fields_list 29 | return StandardAdmin 30 | 31 | 32 | def auto_register(models_module): 33 | from django.contrib import admin 34 | from django.db import models 35 | from django.core.exceptions import ImproperlyConfigured 36 | variables = dir(models_module) 37 | variables = filter(lambda x: not x.startswith('__'), variables) 38 | for x in variables: 39 | value = getattr(models_module, x) 40 | try: 41 | model_class = issubclass(value, models.Model) 42 | except TypeError as e: 43 | model_class = False 44 | if model_class: 45 | try: 46 | admin_config = auto_configure_admin(value) 47 | admin.site.register(value, admin_config) 48 | except Exception as e: 49 | logger.info('couldnt register %s to the admin', value) 50 | -------------------------------------------------------------------------------- /core/utils/loading.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.utils.importlib import import_module 3 | import sys 4 | 5 | 6 | def import_by_path(dotted_path, error_prefix=''): 7 | """ 8 | Import a dotted module path and return the attribute/class designated by the 9 | last name in the path. Raise ImproperlyConfigured if something goes wrong. 10 | """ 11 | try: 12 | module_path, class_name = dotted_path.rsplit('.', 1) 13 | except ValueError: 14 | raise ImproperlyConfigured("%s%s doesn't look like a module path" % ( 15 | error_prefix, dotted_path)) 16 | try: 17 | module = import_module(module_path) 18 | except ImportError as e: 19 | msg = '%sError importing module %s: "%s"' % ( 20 | error_prefix, module_path, e) 21 | raise ImproperlyConfigured 22 | try: 23 | attr = getattr(module, class_name) 24 | except AttributeError: 25 | raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % ( 26 | error_prefix, module_path, class_name)) 27 | return attr 28 | -------------------------------------------------------------------------------- /core/utils/request.py: -------------------------------------------------------------------------------- 1 | from django.test.client import RequestFactory 2 | 3 | 4 | class RequestMock(RequestFactory): 5 | 6 | ''' 7 | Didn't see another solution for this. Decided to read some snippets 8 | and modded them into the requestfactory class 9 | http://www.mellowmorning.com/2011/04/18/mock-django-request-for-testing/ 10 | ''' 11 | 12 | def request(self, **request): 13 | from django.core.handlers.base import BaseHandler 14 | "Construct a generic request object." 15 | request['REQUEST'] = dict() 16 | request = RequestFactory.request(self, **request) 17 | handler = BaseHandler() 18 | handler.load_middleware() 19 | for middleware_method in handler._request_middleware: 20 | if middleware_method(request): 21 | raise Exception("Couldn't create request mock object - " 22 | "request middleware returned a response") 23 | return request 24 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from core import forms 2 | from core.models import Item 3 | from core.models import Follow 4 | from django.contrib.auth import authenticate, get_user_model, \ 5 | login as auth_login 6 | from django.contrib.auth.decorators import login_required 7 | from django.conf import settings 8 | from django.http import HttpResponse, HttpResponseRedirect 9 | from django.shortcuts import render 10 | from stream_django.feed_manager import feed_manager 11 | from core.enrich import Enrich 12 | from core.enrich import did_i_pin_items 13 | from core.enrich import do_i_follow_users 14 | import json 15 | 16 | 17 | def redirect_to_next(request): 18 | return HttpResponseRedirect(request.POST.get('next', '/')) 19 | 20 | 21 | def render_output(output): 22 | ajax_response = HttpResponse( 23 | json.dumps(output), content_type='application/json') 24 | return ajax_response 25 | 26 | 27 | def trending(request): 28 | ''' 29 | The most popular items 30 | ''' 31 | if not request.user.is_authenticated() and not settings.USE_AUTH: 32 | # hack to log you in automatically for the demo app 33 | admin_user = authenticate(username='admin', password='admin') 34 | auth_login(request, admin_user) 35 | 36 | # show a few items 37 | context = {} 38 | popular = Item.objects.all()[:50] 39 | if request.user.is_authenticated(): 40 | did_i_pin_items(request.user, popular) 41 | context['popular'] = popular 42 | response = render(request, 'core/trending.html', context) 43 | return response 44 | 45 | 46 | @login_required 47 | def feed(request): 48 | ''' 49 | Items pinned by the people you follow 50 | ''' 51 | enricher = Enrich(request.user) 52 | context = {} 53 | feed = feed_manager.get_news_feeds(request.user.id)['timeline'] 54 | activities = feed.get(limit=25)['results'] 55 | context['activities'] = enricher.enrich_activities(activities) 56 | response = render(request, 'core/feed.html', context) 57 | return response 58 | 59 | 60 | @login_required 61 | def aggregated_feed(request): 62 | ''' 63 | Items pinned by the people you follow 64 | ''' 65 | enricher = Enrich(request.user) 66 | context = {} 67 | feed = feed_manager.get_news_feeds(request.user.id)['timeline_aggregated'] 68 | activities = feed.get(limit=25)['results'] 69 | context['activities'] = enricher.enrich_aggregated_activities(activities) 70 | response = render(request, 'core/aggregated_feed.html', context) 71 | return response 72 | 73 | 74 | @login_required 75 | def notification_feed(request): 76 | enricher = Enrich(request.user) 77 | context = {} 78 | feed = feed_manager.get_notification_feed(request.user.id) 79 | activities = feed.get(limit=25, mark_seen='all')['results'] 80 | context['activities'] = enricher.enrich_aggregated_activities(activities) 81 | response = render(request, 'core/notification_feed.html', context) 82 | return response 83 | 84 | 85 | def profile(request, username): 86 | ''' 87 | Shows the users profile 88 | ''' 89 | enricher = Enrich(request.user) 90 | profile_user = get_user_model().objects.get(username=username) 91 | feed = feed_manager.get_user_feed(profile_user.id) 92 | activities = feed.get(limit=25)['results'] 93 | context = {} 94 | do_i_follow_users(request.user, [profile_user]) 95 | context['profile_user'] = profile_user 96 | context['activities'] = enricher.enrich_activities(activities) 97 | response = render(request, 'core/profile.html', context) 98 | return response 99 | 100 | 101 | @login_required 102 | def people(request): 103 | context = {} 104 | people = get_user_model().objects.all() 105 | people = people.exclude(username__in=['admin', 'bogus']) 106 | people = people[:25] 107 | do_i_follow_users(request.user, people) 108 | context['people'] = people 109 | response = render(request, 'core/people.html', context) 110 | return response 111 | 112 | 113 | @login_required 114 | def pin(request): 115 | ''' 116 | Simple view to handle (re) pinning an item 117 | ''' 118 | output = {} 119 | if request.method == "POST": 120 | form = forms.PinForm(user=request.user, data=request.POST) 121 | 122 | if form.is_valid(): 123 | pin = form.save() 124 | if pin: 125 | output['pin'] = dict(id=pin.id) 126 | if not request.is_ajax(): 127 | return redirect_to_next(request) 128 | else: 129 | output['errors'] = dict(form.errors.items()) 130 | return render_output(output) 131 | 132 | 133 | @login_required 134 | def follow(request): 135 | ''' 136 | A view to follow other users 137 | ''' 138 | output = {} 139 | if request.method == "POST": 140 | form = forms.FollowForm(user=request.user, data=request.POST) 141 | 142 | if form.is_valid(): 143 | follow = form.save() 144 | if follow: 145 | output['follow'] = dict(id=follow.id) 146 | if not request.is_ajax(): 147 | return redirect_to_next(request) 148 | else: 149 | output['errors'] = dict(form.errors.items()) 150 | return HttpResponse(json.dumps(output), content_type='application/json') 151 | 152 | 153 | @login_required 154 | def auto_follow(request): 155 | ''' 156 | silly view that let a random user follow current user 157 | ''' 158 | admin = get_user_model().objects.get(username='tbarbugli') 159 | Follow.objects.get_or_create(user=admin, target_id=request.user.id) 160 | return HttpResponse('{}', content_type='application/json') 161 | -------------------------------------------------------------------------------- /core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for pinterest_example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | 16 | try: 17 | from dj_static import Cling 18 | except ImportError: 19 | pass 20 | else: 21 | application = Cling(application) 22 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mock 3 | -------------------------------------------------------------------------------- /install.md: -------------------------------------------------------------------------------- 1 | 1. create and activate a virtual environment 2 | 2. install requirements with `pip install -r requirements.txt` 3 | 3. create an account on https://getstream.io 4 | 4. Use the default created app or create a new one with the feeds with the 5 | following names and types: 6 | 7 | a. user, flat 8 | b. notification, notification 9 | c. timline, flat 10 | d. timline_aggregated, aggregated 11 | 12 | 5. get your API credentials (key and secret) and add them to the bottom of 13 | `core/settings.py` (STREAM_API_KEY and STREAM_API_SECRET) 14 | 6. Change the following settings in `core/settings.py`: 15 | 16 | ```python 17 | DEBUG = True 18 | 19 | ... 20 | 21 | DATABASES = { 22 | 'default': { 23 | 'ENGINE': 'django.db.backends.sqlite3', 24 | 'NAME': 'testdatabase.sqlite', 25 | } 26 | } 27 | 28 | ``` 29 | 30 | 7. install ruby dependencies (compass) via bundler `bundler` or with gem `gem install compass` 31 | 8. initialize your app `python manage.py after_deploy` 32 | 9. collect static files `python manage.py collectstatic` 33 | 10. compress the static files `python manage.py compress` 34 | 11. start the webserver `python manage.py runserver` 35 | 12. open your browser on http://localhost:8000 36 | 37 | If you have any problems please open a issue on github and paste any error you get in the console. 38 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault( 6 | "DJANGO_SETTINGS_MODULE", "core.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /media/items/011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/011.jpg -------------------------------------------------------------------------------- /media/items/144568575661705981_4ade117f9005.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/144568575661705981_4ade117f9005.jpeg -------------------------------------------------------------------------------- /media/items/164471358120532732_a3ae9498ad97.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/164471358120532732_a3ae9498ad97.jpg -------------------------------------------------------------------------------- /media/items/167111313523218252_a6046175bb32.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/167111313523218252_a6046175bb32.jpeg -------------------------------------------------------------------------------- /media/items/184290639033145973_71e492dacec6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/184290639033145973_71e492dacec6.jpeg -------------------------------------------------------------------------------- /media/items/1addc6292e97f5f796cf368df5d4b322.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/1addc6292e97f5f796cf368df5d4b322.jpg -------------------------------------------------------------------------------- /media/items/261379381_d8fde25053fa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/261379381_d8fde25053fa.jpg -------------------------------------------------------------------------------- /media/items/272236897059737539_c4806e0b14c4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/272236897059737539_c4806e0b14c4.jpg -------------------------------------------------------------------------------- /media/items/272236897059737539_c4806e0b14c4_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/272236897059737539_c4806e0b14c4_1.jpg -------------------------------------------------------------------------------- /media/items/275088534757448505_4f85768f71ec.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/275088534757448505_4f85768f71ec.jpg -------------------------------------------------------------------------------- /media/items/281485446934954481_c04cfc76c018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/281485446934954481_c04cfc76c018.png -------------------------------------------------------------------------------- /media/items/293704645_b4c6dab8ed99.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/293704645_b4c6dab8ed99.jpg -------------------------------------------------------------------------------- /media/items/389230669370236857_335b7c698687.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/389230669370236857_335b7c698687.jpg -------------------------------------------------------------------------------- /media/items/6fb3f77d22ddc23305fcd1a2fd8c4e80.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/6fb3f77d22ddc23305fcd1a2fd8c4e80.jpg -------------------------------------------------------------------------------- /media/items/6fb3f77d22ddc23305fcd1a2fd8c4e80_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/6fb3f77d22ddc23305fcd1a2fd8c4e80_1.jpg -------------------------------------------------------------------------------- /media/items/70602b9d4f965eee1d0345d99622798f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/70602b9d4f965eee1d0345d99622798f.jpg -------------------------------------------------------------------------------- /media/items/73f98aae32e30a729bdf1031a78db65a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/73f98aae32e30a729bdf1031a78db65a.jpg -------------------------------------------------------------------------------- /media/items/e2110e4545f1cc11043659fdd9c07b43.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/e2110e4545f1cc11043659fdd9c07b43.jpg -------------------------------------------------------------------------------- /media/items/e315a43813c5533b9f47754406019e24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/Stream-Example-Py/dd97d03fd3edc088aa8be162fb9eaf4af2e663df/media/items/e315a43813c5533b9f47754406019e24.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.10.2,<1.11 2 | stream-django>=1.3.1,<1.4 3 | pillow 4 | django_compressor 5 | pytz 6 | django-allauth>=0.27.0,<0.28 7 | sorl-thumbnail 8 | rollbar 9 | django-toolbelt 10 | dj-database-url 11 | -------------------------------------------------------------------------------- /static/CACHE/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "052b341e4289138ad55256000a904975": "" 3 | } -------------------------------------------------------------------------------- /static/css/_feedly.scss: -------------------------------------------------------------------------------- 1 | // MEDIA QUERIES 2 | // -------------------------------------------------- 3 | @mixin respond-to($size, $operator: 'min', $unit: 'px', $query: 'only screen', $unitToRender:'') { 4 | @if unitless($size) { 5 | $unitToRender: 'px' 6 | } @else { 7 | $unitToRender: '' 8 | } 9 | @media #{$query} and (#{$operator}-width: #{$size}#{$unitToRender}) { 10 | @content; 11 | } 12 | } 13 | 14 | @mixin column-gap($size) { 15 | -webkit-column-gap: $size#{'px'}; 16 | -moz-column-gap: $size#{'px'}; 17 | column-gap: $size#{'px'}; 18 | } 19 | 20 | @mixin column-count($quantity) { 21 | -webkit-column-count: $quantity; 22 | -moz-column-count: $quantity; 23 | column-count: $quantity; 24 | } 25 | 26 | #notification-container { 27 | width: 300px; 28 | } 29 | 30 | .badge { 31 | background-color: #d2322d; 32 | } 33 | 34 | body { 35 | min-height: 2000px; 36 | padding-top: 50px; 37 | } 38 | 39 | #wrapper { 40 | max-width: 1100px; 41 | margin: 50px auto; 42 | padding: 0 15px; 43 | } 44 | 45 | .container-pins { 46 | 47 | @include respond-to(480) { 48 | @include column-gap(15); 49 | @include column-count(2); 50 | } 51 | @include respond-to(768) { 52 | @include column-count(3); 53 | } 54 | @include respond-to(1024) { 55 | &:not(.profile) { 56 | @include column-count(4); 57 | } 58 | } 59 | @include respond-to(1100) { 60 | &:not(.profile) { 61 | @include column-count(5); 62 | } 63 | } 64 | } 65 | 66 | .aggregation { 67 | background: #fff; 68 | padding: $grid-gutter-width; 69 | margin-bottom: $grid-gutter-width; 70 | box-shadow: 0 1px 7px 2px rgba(0,0,0,.1); 71 | 72 | & .pin { 73 | max-width: 270px; 74 | margin-right: $grid-gutter-width / 2; 75 | box-shadow: none; 76 | border: 1px solid #eee; 77 | } 78 | & .pin-image { 79 | max-width: 100%; 80 | height: auto; 81 | width: auto; 82 | } 83 | 84 | & .pin-image-holder { 85 | width: auto; 86 | max-height: 270px; 87 | } 88 | } 89 | 90 | .aggregation-header { 91 | height: 45px; 92 | line-height: 45px; 93 | margin-bottom: $grid-gutter-width / 2; 94 | text-transform: uppercase; 95 | } 96 | 97 | .aggregation-time { 98 | text-transform: none; 99 | font-size: 10px; 100 | color: #999; 101 | } 102 | 103 | .pin { 104 | display: inline-block; 105 | background: #fff; 106 | box-shadow: 0 1px 7px 2px rgba(0,0,0,.1); 107 | margin: 0 0 $grid-gutter-width / 2; 108 | -webkit-column-break-inside: avoid; 109 | -moz-column-break-inside: avoid; 110 | column-break-inside: avoid; 111 | @include transition(all .2s ease); 112 | 113 | & textarea { 114 | resize: vertical; 115 | } 116 | } 117 | 118 | .pin-image { 119 | width: 100%; 120 | } 121 | 122 | .pin-image-holder { 123 | overflow: hidden; 124 | width: 100%; 125 | box-shadow: inset 0 1px 2px rgba(0,0,0,.2); 126 | border-bottom: 1px solid rgba(0,0,0,.1); 127 | } 128 | 129 | .pin-caption { 130 | padding: $grid-gutter-width / 2 $grid-gutter-width / 2 0 $grid-gutter-width / 2; 131 | color: $gray; 132 | font-size: 12px; 133 | line-height: 17px; 134 | word-wrap: break-word; 135 | } 136 | 137 | .follow-caption { 138 | padding: $grid-gutter-width / 2 $grid-gutter-width / 2 $grid-gutter-width / 2 $grid-gutter-width / 2; 139 | color: $gray; 140 | font-size: 12px; 141 | line-height: 17px; 142 | word-wrap: break-word; 143 | } 144 | 145 | .pin-bottom { 146 | padding: $grid-gutter-width / 2; 147 | } 148 | 149 | .pin-attribution { 150 | white-space: nowrap; 151 | overflow: hidden; 152 | -ms-text-overflow: ellipsis; 153 | -o-text-overflow: ellipsis; 154 | text-overflow: ellipsis; 155 | color: #717171; 156 | display: block; 157 | color: $gray; 158 | font-size: 10px; 159 | padding: 5px 15px; 160 | background: #f5f5f5; 161 | border-top: 1px solid #eee; 162 | text-transform: uppercase; 163 | } -------------------------------------------------------------------------------- /static/css/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Twitter, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /static/css/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headingsColor 19 | color: inherit; 20 | } 21 | // Provide class for links that match alerts 22 | .alert-link { 23 | font-weight: $alert-link-font-weight; 24 | } 25 | 26 | // Improve alignment and spacing of inner content 27 | > p, 28 | > ul { 29 | margin-bottom: 0; 30 | } 31 | > p + p { 32 | margin-top: 5px; 33 | } 34 | } 35 | 36 | // Dismissable alerts 37 | // 38 | // Expand the right padding and account for the close button's positioning. 39 | 40 | .alert-dismissable { 41 | padding-right: ($alert-padding + 20); 42 | 43 | // Adjust close link position 44 | .close { 45 | position: relative; 46 | top: -2px; 47 | right: -21px; 48 | color: inherit; 49 | } 50 | } 51 | 52 | // Alternate styles 53 | // 54 | // Generate contextual modifier classes for colorizing the alert. 55 | 56 | .alert-success { 57 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 58 | } 59 | .alert-info { 60 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 61 | } 62 | .alert-warning { 63 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 64 | } 65 | .alert-danger { 66 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 67 | } 68 | -------------------------------------------------------------------------------- /static/css/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base classes 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: baseline; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | } 26 | 27 | // Hover state, but only for links 28 | a.badge { 29 | &:hover, 30 | &:focus { 31 | color: $badge-link-hover-color; 32 | text-decoration: none; 33 | cursor: pointer; 34 | } 35 | } 36 | 37 | // Quick fix for labels/badges in buttons 38 | .btn .badge { 39 | position: relative; 40 | top: -1px; 41 | } 42 | 43 | // Account for counters in navs 44 | a.list-group-item.active > .badge, 45 | .nav-pills > .active > a > .badge { 46 | color: $badge-active-color; 47 | background-color: $badge-active-bg; 48 | } 49 | .nav-pills > li > a > .badge { 50 | margin-left: 3px; 51 | } 52 | -------------------------------------------------------------------------------- /static/css/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: 8px 15px; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | > li { 13 | display: inline-block; 14 | &+li:before { 15 | content: "/\00a0"; // Unicode space added since inline-block means non-collapsing white-space 16 | padding: 0 5px; 17 | color: $breadcrumb-color; 18 | } 19 | } 20 | > .active { 21 | color: $breadcrumb-active-color; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/css/bootstrap/_button-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Button groups 3 | // -------------------------------------------------- 4 | 5 | // Button carets 6 | // 7 | // Match the button text color to the arrow/caret for indicating dropdown-ness. 8 | 9 | .caret { 10 | .btn-default & { 11 | border-top-color: $btn-default-color; 12 | } 13 | .btn-primary &, 14 | .btn-success &, 15 | .btn-warning &, 16 | .btn-danger &, 17 | .btn-info & { 18 | border-top-color: #fff; 19 | } 20 | } 21 | .dropup { 22 | & .btn-default .caret { 23 | border-bottom-color: $btn-default-color; 24 | } 25 | .btn-primary, 26 | .btn-success, 27 | .btn-warning, 28 | .btn-danger, 29 | .btn-info { 30 | .caret { 31 | border-bottom-color: #fff; 32 | } 33 | } 34 | } 35 | 36 | // Make the div behave like a button 37 | .btn-group, 38 | .btn-group-vertical { 39 | position: relative; 40 | display: inline-block; 41 | vertical-align: middle; // match .btn alignment given font-size hack above 42 | > .btn { 43 | position: relative; 44 | float: left; 45 | // Bring the "active" button to the front 46 | &:hover, 47 | &:focus, 48 | &:active, 49 | &.active { 50 | z-index: 2; 51 | } 52 | &:focus { 53 | // Remove focus outline when dropdown JS adds it after closing the menu 54 | outline: none; 55 | } 56 | } 57 | } 58 | 59 | // Prevent double borders when buttons are next to each other 60 | .btn-group { 61 | .btn + .btn, 62 | .btn + .btn-group, 63 | .btn-group + .btn, 64 | .btn-group + .btn-group { 65 | margin-left: -1px; 66 | } 67 | } 68 | 69 | // Optional: Group multiple button groups together for a toolbar 70 | .btn-toolbar { 71 | @include clearfix(); 72 | 73 | .btn-group { 74 | float: left; 75 | } 76 | // Space out series of button groups 77 | > .btn, 78 | > .btn-group { 79 | + .btn, 80 | + .btn-group { 81 | margin-left: 5px; 82 | } 83 | } 84 | } 85 | 86 | .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { 87 | border-radius: 0; 88 | } 89 | 90 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match 91 | .btn-group > .btn:first-child { 92 | margin-left: 0; 93 | &:not(:last-child):not(.dropdown-toggle) { 94 | @include border-right-radius(0); 95 | } 96 | } 97 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it 98 | .btn-group > .btn:last-child:not(:first-child), 99 | .btn-group > .dropdown-toggle:not(:first-child) { 100 | @include border-left-radius(0); 101 | } 102 | 103 | // Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) 104 | .btn-group > .btn-group { 105 | float: left; 106 | } 107 | .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { 108 | border-radius: 0; 109 | } 110 | .btn-group > .btn-group:first-child { 111 | > .btn:last-child, 112 | > .dropdown-toggle { 113 | @include border-right-radius(0); 114 | } 115 | } 116 | .btn-group > .btn-group:last-child > .btn:first-child { 117 | @include border-left-radius(0); 118 | } 119 | 120 | // On active and open, don't show outline 121 | .btn-group .dropdown-toggle:active, 122 | .btn-group.open .dropdown-toggle { 123 | outline: 0; 124 | } 125 | 126 | 127 | // Sizing 128 | // 129 | // Remix the default button sizing classes into new ones for easier manipulation. 130 | 131 | .btn-group-xs > .btn { @extend .btn-xs; } 132 | .btn-group-sm > .btn { @extend .btn-sm; } 133 | .btn-group-lg > .btn { @extend .btn-lg; } 134 | 135 | 136 | // Split button dropdowns 137 | // ---------------------- 138 | 139 | // Give the line between buttons some depth 140 | .btn-group > .btn + .dropdown-toggle { 141 | padding-left: 8px; 142 | padding-right: 8px; 143 | } 144 | .btn-group > .btn-lg + .dropdown-toggle { 145 | padding-left: 12px; 146 | padding-right: 12px; 147 | } 148 | 149 | // The clickable button for toggling the menu 150 | // Remove the gradient and set the same inset shadow as the :active state 151 | .btn-group.open .dropdown-toggle { 152 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 153 | } 154 | 155 | 156 | // Reposition the caret 157 | .btn .caret { 158 | margin-left: 0; 159 | } 160 | // Carets in other button sizes 161 | .btn-lg .caret { 162 | border-width: $caret-width-large $caret-width-large 0; 163 | border-bottom-width: 0; 164 | } 165 | // Upside down carets for .dropup 166 | .dropup .btn-lg .caret { 167 | border-width: 0 $caret-width-large $caret-width-large; 168 | } 169 | 170 | 171 | // Vertical button groups 172 | // ---------------------- 173 | 174 | .btn-group-vertical { 175 | > .btn, 176 | > .btn-group { 177 | display: block; 178 | float: none; 179 | width: 100%; 180 | max-width: 100%; 181 | } 182 | 183 | // Clear floats so dropdown menus can be properly placed 184 | > .btn-group { 185 | @include clearfix(); 186 | > .btn { 187 | float: none; 188 | } 189 | } 190 | 191 | > .btn + .btn, 192 | > .btn + .btn-group, 193 | > .btn-group + .btn, 194 | > .btn-group + .btn-group { 195 | margin-top: -1px; 196 | margin-left: 0; 197 | } 198 | } 199 | 200 | .btn-group-vertical > .btn { 201 | &:not(:first-child):not(:last-child) { 202 | border-radius: 0; 203 | } 204 | &:first-child:not(:last-child) { 205 | border-top-right-radius: $border-radius-base; 206 | @include border-bottom-radius(0); 207 | } 208 | &:last-child:not(:first-child) { 209 | border-bottom-left-radius: $border-radius-base; 210 | @include border-top-radius(0); 211 | } 212 | } 213 | .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { 214 | border-radius: 0; 215 | } 216 | .btn-group-vertical > .btn-group:first-child { 217 | > .btn:last-child, 218 | > .dropdown-toggle { 219 | @include border-bottom-radius(0); 220 | } 221 | } 222 | .btn-group-vertical > .btn-group:last-child > .btn:first-child { 223 | @include border-top-radius(0); 224 | } 225 | 226 | 227 | 228 | // Justified button groups 229 | // ---------------------- 230 | 231 | .btn-group-justified { 232 | display: table; 233 | width: 100%; 234 | table-layout: fixed; 235 | border-collapse: separate; 236 | .btn { 237 | float: none; 238 | display: table-cell; 239 | width: 1%; 240 | } 241 | } 242 | 243 | 244 | // Checkbox and radio options 245 | [data-toggle="buttons"] > .btn > input[type="radio"], 246 | [data-toggle="buttons"] > .btn > input[type="checkbox"] { 247 | display: none; 248 | } 249 | -------------------------------------------------------------------------------- /static/css/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // -------------------------------------------------- 8 | 9 | // Core styles 10 | .btn { 11 | display: inline-block; 12 | padding: $padding-base-vertical $padding-base-horizontal; 13 | margin-bottom: 0; // For input.btn 14 | font-size: $font-size-base; 15 | font-weight: $btn-font-weight; 16 | line-height: $line-height-base; 17 | text-align: center; 18 | vertical-align: middle; 19 | cursor: pointer; 20 | border: 1px solid transparent; 21 | border-radius: $border-radius-base; 22 | white-space: nowrap; 23 | @include user-select(none); 24 | 25 | &:focus { 26 | @include tab-focus(); 27 | } 28 | 29 | &:hover, 30 | &:focus { 31 | color: $btn-default-color; 32 | text-decoration: none; 33 | } 34 | 35 | &:active, 36 | &.active { 37 | outline: 0; 38 | background-image: none; 39 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 40 | } 41 | 42 | &.disabled, 43 | &[disabled], 44 | fieldset[disabled] & { 45 | cursor: not-allowed; 46 | pointer-events: none; // Future-proof disabling of clicks 47 | @include opacity(.65); 48 | @include box-shadow(none); 49 | } 50 | 51 | } 52 | 53 | 54 | // Alternate buttons 55 | // -------------------------------------------------- 56 | 57 | .btn-default { 58 | @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); 59 | } 60 | .btn-primary { 61 | @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); 62 | } 63 | // Warning appears as orange 64 | .btn-warning { 65 | @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); 66 | } 67 | // Danger and error appear as red 68 | .btn-danger { 69 | @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); 70 | } 71 | // Success appears as green 72 | .btn-success { 73 | @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); 74 | } 75 | // Info appears as blue-green 76 | .btn-info { 77 | @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); 78 | } 79 | 80 | 81 | // Link buttons 82 | // ------------------------- 83 | 84 | // Make a button look and behave like a link 85 | .btn-link { 86 | color: $link-color; 87 | font-weight: normal; 88 | cursor: pointer; 89 | border-radius: 0; 90 | 91 | &, 92 | &:active, 93 | &[disabled], 94 | fieldset[disabled] & { 95 | background-color: transparent; 96 | @include box-shadow(none); 97 | } 98 | &, 99 | &:hover, 100 | &:focus, 101 | &:active { 102 | border-color: transparent; 103 | } 104 | &:hover, 105 | &:focus { 106 | color: $link-hover-color; 107 | text-decoration: underline; 108 | background-color: transparent; 109 | } 110 | &[disabled], 111 | fieldset[disabled] & { 112 | &:hover, 113 | &:focus { 114 | color: $btn-link-disabled-color; 115 | text-decoration: none; 116 | } 117 | } 118 | } 119 | 120 | 121 | // Button Sizes 122 | // -------------------------------------------------- 123 | 124 | .btn-lg { 125 | // line-height: ensure even-numbered height of button next to large input 126 | @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); 127 | } 128 | .btn-sm, 129 | .btn-xs { 130 | // line-height: ensure proper height of button next to small input 131 | @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); 132 | } 133 | .btn-xs { 134 | padding: 1px 5px; 135 | } 136 | 137 | 138 | // Block button 139 | // -------------------------------------------------- 140 | 141 | .btn-block { 142 | display: block; 143 | width: 100%; 144 | padding-left: 0; 145 | padding-right: 0; 146 | } 147 | 148 | // Vertically space out multiple block buttons 149 | .btn-block + .btn-block { 150 | margin-top: 5px; 151 | } 152 | 153 | // Specificity overrides 154 | input[type="submit"], 155 | input[type="reset"], 156 | input[type="button"] { 157 | &.btn-block { 158 | width: 100%; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /static/css/bootstrap/_carousel.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Carousel 3 | // -------------------------------------------------- 4 | 5 | 6 | // Wrapper for the slide container and indicators 7 | .carousel { 8 | position: relative; 9 | } 10 | 11 | .carousel-inner { 12 | position: relative; 13 | overflow: hidden; 14 | width: 100%; 15 | 16 | > .item { 17 | display: none; 18 | position: relative; 19 | @include transition(.6s ease-in-out left); 20 | 21 | // Account for jankitude on images 22 | > img, 23 | > a > img { 24 | @include img-responsive(); 25 | line-height: 1; 26 | } 27 | } 28 | 29 | > .active, 30 | > .next, 31 | > .prev { display: block; } 32 | 33 | > .active { 34 | left: 0; 35 | } 36 | 37 | > .next, 38 | > .prev { 39 | position: absolute; 40 | top: 0; 41 | width: 100%; 42 | } 43 | 44 | > .next { 45 | left: 100%; 46 | } 47 | > .prev { 48 | left: -100%; 49 | } 50 | > .next.left, 51 | > .prev.right { 52 | left: 0; 53 | } 54 | 55 | > .active.left { 56 | left: -100%; 57 | } 58 | > .active.right { 59 | left: 100%; 60 | } 61 | 62 | } 63 | 64 | // Left/right controls for nav 65 | // --------------------------- 66 | 67 | .carousel-control { 68 | position: absolute; 69 | top: 0; 70 | left: 0; 71 | bottom: 0; 72 | width: $carousel-control-width; 73 | @include opacity($carousel-control-opacity); 74 | font-size: $carousel-control-font-size; 75 | color: $carousel-control-color; 76 | text-align: center; 77 | text-shadow: $carousel-text-shadow; 78 | // We can't have this transition here because webkit cancels the carousel 79 | // animation if you trip this while in the middle of another animation. 80 | 81 | // Set gradients for backgrounds 82 | &.left { 83 | @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); 84 | } 85 | &.right { 86 | left: auto; 87 | right: 0; 88 | @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); 89 | } 90 | 91 | // Hover/focus state 92 | &:hover, 93 | &:focus { 94 | color: $carousel-control-color; 95 | text-decoration: none; 96 | @include opacity(.9); 97 | } 98 | 99 | // Toggles 100 | .icon-prev, 101 | .icon-next, 102 | .glyphicon-chevron-left, 103 | .glyphicon-chevron-right { 104 | position: absolute; 105 | top: 50%; 106 | left: 50%; 107 | z-index: 5; 108 | display: inline-block; 109 | } 110 | .icon-prev, 111 | .icon-next { 112 | width: 20px; 113 | height: 20px; 114 | margin-top: -10px; 115 | margin-left: -10px; 116 | font-family: serif; 117 | } 118 | 119 | .icon-prev { 120 | &:before { 121 | content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) 122 | } 123 | } 124 | .icon-next { 125 | &:before { 126 | content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) 127 | } 128 | } 129 | } 130 | 131 | // Optional indicator pips 132 | // 133 | // Add an unordered list with the following class and add a list item for each 134 | // slide your carousel holds. 135 | 136 | .carousel-indicators { 137 | position: absolute; 138 | bottom: 10px; 139 | left: 50%; 140 | z-index: 15; 141 | width: 60%; 142 | margin-left: -30%; 143 | padding-left: 0; 144 | list-style: none; 145 | text-align: center; 146 | 147 | li { 148 | display: inline-block; 149 | width: 10px; 150 | height: 10px; 151 | margin: 1px; 152 | text-indent: -999px; 153 | border: 1px solid $carousel-indicator-border-color; 154 | border-radius: 10px; 155 | cursor: pointer; 156 | } 157 | .active { 158 | margin: 0; 159 | width: 12px; 160 | height: 12px; 161 | background-color: $carousel-indicator-active-bg; 162 | } 163 | } 164 | 165 | // Optional captions 166 | // ----------------------------- 167 | // Hidden by default for smaller viewports 168 | .carousel-caption { 169 | position: absolute; 170 | left: 15%; 171 | right: 15%; 172 | bottom: 20px; 173 | z-index: 10; 174 | padding-top: 20px; 175 | padding-bottom: 20px; 176 | color: $carousel-caption-color; 177 | text-align: center; 178 | text-shadow: $carousel-text-shadow; 179 | & .btn { 180 | text-shadow: none; // No shadow for button elements in carousel-caption 181 | } 182 | } 183 | 184 | 185 | // Scale up controls for tablets and up 186 | @media screen and (min-width: $screen-tablet) { 187 | 188 | // Scale up the controls a smidge 189 | .carousel-control .icon-prev, 190 | .carousel-control .icon-next { 191 | width: 30px; 192 | height: 30px; 193 | margin-top: -15px; 194 | margin-left: -15px; 195 | font-size: 30px; 196 | } 197 | 198 | // Show and left align the captions 199 | .carousel-caption { 200 | left: 20%; 201 | right: 20%; 202 | padding-bottom: 30px; 203 | } 204 | 205 | // Move up the indicators 206 | .carousel-indicators { 207 | bottom: 20px; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /static/css/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | button.close { 30 | padding: 0; 31 | cursor: pointer; 32 | background: transparent; 33 | border: 0; 34 | -webkit-appearance: none; 35 | } 36 | -------------------------------------------------------------------------------- /static/css/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and blocK) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | pre { 9 | font-family: $font-family-monospace; 10 | } 11 | 12 | // Inline code 13 | code { 14 | padding: 2px 4px; 15 | font-size: 90%; 16 | color: $code-color; 17 | background-color: $code-bg; 18 | white-space: nowrap; 19 | border-radius: $border-radius-base; 20 | } 21 | 22 | // Blocks of code 23 | pre { 24 | display: block; 25 | padding: (($line-height-computed - 1) / 2); 26 | margin: 0 0 ($line-height-computed / 2); 27 | font-size: ($font-size-base - 1); // 14px to 13px 28 | line-height: $line-height-base; 29 | word-break: break-all; 30 | word-wrap: break-word; 31 | color: $pre-color; 32 | background-color: $pre-bg; 33 | border: 1px solid $pre-border-color; 34 | border-radius: $border-radius-base; 35 | 36 | // Make prettyprint styles more spaced out for readability 37 | &.prettyprint { 38 | margin-bottom: $line-height-computed; 39 | } 40 | 41 | // Account for some code outputs that place code tags in pre tags 42 | code { 43 | padding: 0; 44 | font-size: inherit; 45 | color: inherit; 46 | white-space: pre-wrap; 47 | background-color: transparent; 48 | border: 0; 49 | } 50 | } 51 | 52 | // Enable scrollable blocks of code 53 | .pre-scrollable { 54 | max-height: $pre-scrollable-max-height; 55 | overflow-y: scroll; 56 | } 57 | -------------------------------------------------------------------------------- /static/css/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | &.in { 21 | display: block; 22 | } 23 | } 24 | .collapsing { 25 | position: relative; 26 | height: 0; 27 | overflow: hidden; 28 | @include transition(height .35s ease); 29 | } 30 | -------------------------------------------------------------------------------- /static/css/bootstrap/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Dropdown menus 3 | // -------------------------------------------------- 4 | 5 | 6 | // Dropdown arrow/caret 7 | .caret { 8 | display: inline-block; 9 | width: 0; 10 | height: 0; 11 | margin-left: 2px; 12 | vertical-align: middle; 13 | border-top: $caret-width-base solid $dropdown-caret-color; 14 | border-right: $caret-width-base solid transparent; 15 | border-left: $caret-width-base solid transparent; 16 | // Firefox fix for https://github.com/twbs/bootstrap/issues/9538. Once fixed, 17 | // we can just straight up remove this. 18 | border-bottom: 0 dotted; 19 | content: ""; 20 | } 21 | 22 | // The dropdown wrapper (div) 23 | .dropdown { 24 | position: relative; 25 | } 26 | 27 | // Prevent the focus on the dropdown toggle when closing dropdowns 28 | .dropdown-toggle:focus { 29 | outline: 0; 30 | } 31 | 32 | // The dropdown menu (ul) 33 | .dropdown-menu { 34 | position: absolute; 35 | top: 100%; 36 | left: 0; 37 | z-index: $zindex-dropdown; 38 | display: none; // none by default, but block on "open" of the menu 39 | float: left; 40 | min-width: 160px; 41 | padding: 5px 0; 42 | margin: 2px 0 0; // override default ul 43 | list-style: none; 44 | font-size: $font-size-base; 45 | background-color: $dropdown-bg; 46 | border: 1px solid $dropdown-fallback-border; // IE8 fallback 47 | border: 1px solid $dropdown-border; 48 | border-radius: $border-radius-base; 49 | @include box-shadow(0 6px 12px rgba(0,0,0,.175)); 50 | background-clip: padding-box; 51 | 52 | // Aligns the dropdown menu to right 53 | &.pull-right { 54 | right: 0; 55 | left: auto; 56 | } 57 | 58 | // Dividers (basically an hr) within the dropdown 59 | .divider { 60 | @include nav-divider($dropdown-divider-bg); 61 | } 62 | 63 | // Links within the dropdown menu 64 | > li > a { 65 | display: block; 66 | padding: 3px 20px; 67 | clear: both; 68 | font-weight: normal; 69 | line-height: $line-height-base; 70 | color: $dropdown-link-color; 71 | white-space: nowrap; // prevent links from randomly breaking onto new lines 72 | } 73 | } 74 | 75 | // Hover/Focus state 76 | .dropdown-menu > li > a { 77 | &:hover, 78 | &:focus { 79 | text-decoration: none; 80 | color: $dropdown-link-hover-color; 81 | background-color: $dropdown-link-hover-bg; 82 | } 83 | } 84 | 85 | // Active state 86 | .dropdown-menu > .active > a { 87 | &, 88 | &:hover, 89 | &:focus { 90 | color: $dropdown-link-active-color; 91 | text-decoration: none; 92 | outline: 0; 93 | background-color: $dropdown-link-active-bg; 94 | } 95 | } 96 | 97 | // Disabled state 98 | // 99 | // Gray out text and ensure the hover/focus state remains gray 100 | 101 | .dropdown-menu > .disabled > a { 102 | &, 103 | &:hover, 104 | &:focus { 105 | color: $dropdown-link-disabled-color; 106 | } 107 | } 108 | // Nuke hover/focus effects 109 | .dropdown-menu > .disabled > a { 110 | &:hover, 111 | &:focus { 112 | text-decoration: none; 113 | background-color: transparent; 114 | background-image: none; // Remove CSS gradient 115 | @include reset-filter(); 116 | cursor: not-allowed; 117 | } 118 | } 119 | 120 | // Open state for the dropdown 121 | .open { 122 | // Show the menu 123 | > .dropdown-menu { 124 | display: block; 125 | } 126 | 127 | // Remove the outline when :focus is triggered 128 | > a { 129 | outline: 0; 130 | } 131 | } 132 | 133 | // Dropdown section headers 134 | .dropdown-header { 135 | display: block; 136 | padding: 3px 20px; 137 | font-size: $font-size-small; 138 | line-height: $line-height-base; 139 | color: $dropdown-header-color; 140 | } 141 | 142 | // Backdrop to catch body clicks on mobile, etc. 143 | .dropdown-backdrop { 144 | position: fixed; 145 | left: 0; 146 | right: 0; 147 | bottom: 0; 148 | top: 0; 149 | z-index: $zindex-dropdown - 10; 150 | } 151 | 152 | // Right aligned dropdowns 153 | .pull-right > .dropdown-menu { 154 | right: 0; 155 | left: auto; 156 | } 157 | 158 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 159 | // 160 | // Just add .dropup after the standard .dropdown class and you're set, bro. 161 | // TODO: abstract this so that the navbar fixed styles are not placed here? 162 | 163 | .dropup, 164 | .navbar-fixed-bottom .dropdown { 165 | // Reverse the caret 166 | .caret { 167 | // Firefox fix for https://github.com/twbs/bootstrap/issues/9538. Once this 168 | // gets fixed, restore `border-top: 0;`. 169 | border-top: 0 dotted; 170 | border-bottom: 4px solid $dropdown-caret-color; 171 | content: ""; 172 | } 173 | // Different positioning for bottom up menu 174 | .dropdown-menu { 175 | top: auto; 176 | bottom: 100%; 177 | margin-bottom: 1px; 178 | } 179 | } 180 | 181 | 182 | // Component alignment 183 | // 184 | // Reiterate per navbar.less and the modified component alignment there. 185 | 186 | @media (min-width: $grid-float-breakpoint) { 187 | .navbar-right { 188 | .dropdown-menu { 189 | right: 0; 190 | left: auto; 191 | } 192 | } 193 | } 194 | 195 | -------------------------------------------------------------------------------- /static/css/bootstrap/_forms.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Forms 3 | // -------------------------------------------------- 4 | 5 | 6 | // Normalize non-controls 7 | // 8 | // Restyle and baseline non-control form elements. 9 | 10 | fieldset { 11 | padding: 0; 12 | margin: 0; 13 | border: 0; 14 | } 15 | 16 | legend { 17 | display: block; 18 | width: 100%; 19 | padding: 0; 20 | margin-bottom: $line-height-computed; 21 | font-size: ($font-size-base * 1.5); 22 | line-height: inherit; 23 | color: $legend-color; 24 | border: 0; 25 | border-bottom: 1px solid $legend-border-color; 26 | } 27 | 28 | label { 29 | display: inline-block; 30 | margin-bottom: 5px; 31 | font-weight: bold; 32 | } 33 | 34 | 35 | // Normalize form controls 36 | 37 | // Override content-box in Normalize (* isn't specific enough) 38 | input[type="search"] { 39 | @include box-sizing(border-box); 40 | } 41 | 42 | // Position radios and checkboxes better 43 | input[type="radio"], 44 | input[type="checkbox"] { 45 | margin: 4px 0 0; 46 | margin-top: 1px \9; /* IE8-9 */ 47 | line-height: normal; 48 | } 49 | 50 | // Set the height of select and file controls to match text inputs 51 | input[type="file"] { 52 | display: block; 53 | } 54 | 55 | // Make multiple select elements height not fixed 56 | select[multiple], 57 | select[size] { 58 | height: auto; 59 | } 60 | 61 | // Fix optgroup Firefox bug per https://github.com/twbs/bootstrap/issues/7611 62 | select optgroup { 63 | font-size: inherit; 64 | font-style: inherit; 65 | font-family: inherit; 66 | } 67 | 68 | // Focus for select, file, radio, and checkbox 69 | input[type="file"]:focus, 70 | input[type="radio"]:focus, 71 | input[type="checkbox"]:focus { 72 | @include tab-focus(); 73 | } 74 | 75 | // Fix for Chrome number input 76 | // Setting certain font-sizes causes the `I` bar to appear on hover of the bottom increment button. 77 | // See https://github.com/twbs/bootstrap/issues/8350 for more. 78 | input[type="number"] { 79 | &::-webkit-outer-spin-button, 80 | &::-webkit-inner-spin-button { 81 | height: auto; 82 | } 83 | } 84 | 85 | 86 | // Placeholder 87 | // 88 | // Placeholder text gets special styles because when browsers invalidate entire 89 | // lines if it doesn't understand a selector/ 90 | .form-control { 91 | @include placeholder(); 92 | } 93 | 94 | 95 | // Common form controls 96 | // 97 | // Shared size and type resets for form controls. Apply `.form-control` to any 98 | // of the following form controls: 99 | // 100 | // select 101 | // textarea 102 | // input[type="text"] 103 | // input[type="password"] 104 | // input[type="datetime"] 105 | // input[type="datetime-local"] 106 | // input[type="date"] 107 | // input[type="month"] 108 | // input[type="time"] 109 | // input[type="week"] 110 | // input[type="number"] 111 | // input[type="email"] 112 | // input[type="url"] 113 | // input[type="search"] 114 | // input[type="tel"] 115 | // input[type="color"] 116 | 117 | .form-control { 118 | display: block; 119 | width: 100%; 120 | height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) 121 | padding: $padding-base-vertical $padding-base-horizontal; 122 | font-size: $font-size-base; 123 | line-height: $line-height-base; 124 | color: $input-color; 125 | vertical-align: middle; 126 | background-color: $input-bg; 127 | border: 1px solid $input-border; 128 | border-radius: $input-border-radius; 129 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); 130 | @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); 131 | 132 | // Customize the `:focus` state to imitate native WebKit styles. 133 | @include form-control-focus(); 134 | 135 | // Disabled and read-only inputs 136 | // Note: HTML5 says that controls under a fieldset > legend:first-child won't 137 | // be disabled if the fieldset is disabled. Due to implementation difficulty, 138 | // we don't honor that edge case; we style them as disabled anyway. 139 | &[disabled], 140 | &[readonly], 141 | fieldset[disabled] & { 142 | cursor: not-allowed; 143 | background-color: $input-bg-disabled; 144 | } 145 | 146 | // [converter] extracted textarea& to textarea.form-control 147 | } 148 | 149 | // Reset height for `textarea`s 150 | textarea.form-control { 151 | height: auto; 152 | } 153 | 154 | 155 | // Form groups 156 | // 157 | // Designed to help with the organization and spacing of vertical forms. For 158 | // horizontal forms, use the predefined grid classes. 159 | 160 | .form-group { 161 | margin-bottom: 15px; 162 | } 163 | 164 | 165 | // Checkboxes and radios 166 | // 167 | // Indent the labels to position radios/checkboxes as hanging controls. 168 | 169 | .radio, 170 | .checkbox { 171 | display: block; 172 | min-height: $line-height-computed; // clear the floating input if there is no label text 173 | margin-top: 10px; 174 | margin-bottom: 10px; 175 | padding-left: 20px; 176 | vertical-align: middle; 177 | label { 178 | display: inline; 179 | margin-bottom: 0; 180 | font-weight: normal; 181 | cursor: pointer; 182 | } 183 | } 184 | .radio input[type="radio"], 185 | .radio-inline input[type="radio"], 186 | .checkbox input[type="checkbox"], 187 | .checkbox-inline input[type="checkbox"] { 188 | float: left; 189 | margin-left: -20px; 190 | } 191 | .radio + .radio, 192 | .checkbox + .checkbox { 193 | margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing 194 | } 195 | 196 | // Radios and checkboxes on same line 197 | .radio-inline, 198 | .checkbox-inline { 199 | display: inline-block; 200 | padding-left: 20px; 201 | margin-bottom: 0; 202 | vertical-align: middle; 203 | font-weight: normal; 204 | cursor: pointer; 205 | } 206 | .radio-inline + .radio-inline, 207 | .checkbox-inline + .checkbox-inline { 208 | margin-top: 0; 209 | margin-left: 10px; // space out consecutive inline controls 210 | } 211 | 212 | // Apply same disabled cursor tweak as for inputs 213 | // 214 | // Note: Neither radios nor checkboxes can be readonly. 215 | input[type="radio"], 216 | input[type="checkbox"], 217 | .radio, 218 | .radio-inline, 219 | .checkbox, 220 | .checkbox-inline { 221 | &[disabled], 222 | fieldset[disabled] & { 223 | cursor: not-allowed; 224 | } 225 | } 226 | 227 | // Form control sizing 228 | 229 | @include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); 230 | 231 | @include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); 232 | 233 | 234 | // Form control feedback states 235 | // 236 | // Apply contextual and semantic states to individual form controls. 237 | 238 | // Warning 239 | .has-warning { 240 | @include form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg); 241 | } 242 | // Error 243 | .has-error { 244 | @include form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg); 245 | } 246 | // Success 247 | .has-success { 248 | @include form-control-validation($state-success-text, $state-success-text, $state-success-bg); 249 | } 250 | 251 | 252 | // Static form control text 253 | // 254 | // Apply class to a `p` element to make any string of text align with labels in 255 | // a horizontal form layout. 256 | 257 | .form-control-static { 258 | margin-bottom: 0; // Remove default margin from `p` 259 | padding-top: ($padding-base-vertical + 1); 260 | } 261 | 262 | 263 | // Help text 264 | // 265 | // Apply to any element you wish to create light text for placement immediately 266 | // below a form control. Use for general help, formatting, or instructional text. 267 | 268 | .help-block { 269 | display: block; // account for any element using help-block 270 | margin-top: 5px; 271 | margin-bottom: 10px; 272 | color: lighten($text-color, 25%); // lighten the text some for contrast 273 | } 274 | 275 | 276 | 277 | // Inline forms 278 | // 279 | // Make forms appear inline(-block) by adding the `.form-inline` class. Inline 280 | // forms begin stacked on extra small (mobile) devices and then go inline when 281 | // viewports reach <768px. 282 | // 283 | // Requires wrapping inputs and labels with `.form-group` for proper display of 284 | // default HTML form controls and our custom form controls (e.g., input groups). 285 | // 286 | // Heads up! This is mixin-ed into `.navbar-form` in navbars.less. 287 | 288 | .form-inline { 289 | 290 | // Kick in the inline 291 | @media (min-width: $screen-tablet) { 292 | // Inline-block all the things for "inline" 293 | .form-group { 294 | display: inline-block; 295 | margin-bottom: 0; 296 | vertical-align: middle; 297 | } 298 | 299 | // In navbar-form, allow folks to *not* use `.form-group` 300 | .form-control { 301 | display: inline-block; 302 | } 303 | 304 | // Remove default margin on radios/checkboxes that were used for stacking, and 305 | // then undo the floating of radios and checkboxes to match (which also avoids 306 | // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969). 307 | .radio, 308 | .checkbox { 309 | display: inline-block; 310 | margin-top: 0; 311 | margin-bottom: 0; 312 | padding-left: 0; 313 | } 314 | .radio input[type="radio"], 315 | .checkbox input[type="checkbox"] { 316 | float: none; 317 | margin-left: 0; 318 | } 319 | } 320 | } 321 | 322 | 323 | // Horizontal forms 324 | // 325 | // Horizontal forms are built on grid classes and allow you to create forms with 326 | // labels on the left and inputs on the right. 327 | 328 | .form-horizontal { 329 | 330 | // Consistent vertical alignment of labels, radios, and checkboxes 331 | .control-label, 332 | .radio, 333 | .checkbox, 334 | .radio-inline, 335 | .checkbox-inline { 336 | margin-top: 0; 337 | margin-bottom: 0; 338 | padding-top: ($padding-base-vertical + 1); // Default padding plus a border 339 | } 340 | 341 | // Make form groups behave like rows 342 | .form-group { 343 | @include make-row(); 344 | } 345 | 346 | // Only right align form labels here when the columns stop stacking 347 | @media (min-width: $screen-tablet) { 348 | .control-label { 349 | text-align: right; 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /static/css/bootstrap/_input-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Input groups 3 | // -------------------------------------------------- 4 | 5 | // Base styles 6 | // ------------------------- 7 | .input-group { 8 | position: relative; // For dropdowns 9 | display: table; 10 | border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table 11 | 12 | // Undo padding and float of grid classes 13 | &.col { 14 | float: none; 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .form-control { 20 | width: 100%; 21 | margin-bottom: 0; 22 | } 23 | } 24 | 25 | // Sizing options 26 | // 27 | // Remix the default form control sizing classes into new ones for easier 28 | // manipulation. 29 | 30 | .input-group-lg > .form-control, 31 | .input-group-lg > .input-group-addon, 32 | .input-group-lg > .input-group-btn > .btn { @extend .input-lg; } 33 | .input-group-sm > .form-control, 34 | .input-group-sm > .input-group-addon, 35 | .input-group-sm > .input-group-btn > .btn { @extend .input-sm; } 36 | 37 | 38 | // Display as table-cell 39 | // ------------------------- 40 | .input-group-addon, 41 | .input-group-btn, 42 | .input-group .form-control { 43 | display: table-cell; 44 | 45 | &:not(:first-child):not(:last-child) { 46 | border-radius: 0; 47 | } 48 | } 49 | // Addon and addon wrapper for buttons 50 | .input-group-addon, 51 | .input-group-btn { 52 | width: 1%; 53 | white-space: nowrap; 54 | vertical-align: middle; // Match the inputs 55 | } 56 | 57 | // Text input groups 58 | // ------------------------- 59 | .input-group-addon { 60 | padding: $padding-base-vertical $padding-base-horizontal; 61 | font-size: $font-size-base; 62 | font-weight: normal; 63 | line-height: 1; 64 | text-align: center; 65 | background-color: $input-group-addon-bg; 66 | border: 1px solid $input-group-addon-border-color; 67 | border-radius: $border-radius-base; 68 | 69 | // Sizing 70 | &.input-sm { 71 | padding: $padding-small-vertical $padding-small-horizontal; 72 | font-size: $font-size-small; 73 | border-radius: $border-radius-small; 74 | } 75 | &.input-lg { 76 | padding: $padding-large-vertical $padding-large-horizontal; 77 | font-size: $font-size-large; 78 | border-radius: $border-radius-large; 79 | } 80 | 81 | // Nuke default margins from checkboxes and radios to vertically center within. 82 | input[type="radio"], 83 | input[type="checkbox"] { 84 | margin-top: 0; 85 | } 86 | } 87 | 88 | // Reset rounded corners 89 | .input-group .form-control:first-child, 90 | .input-group-addon:first-child, 91 | .input-group-btn:first-child > .btn, 92 | .input-group-btn:first-child > .dropdown-toggle, 93 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { 94 | @include border-right-radius(0); 95 | } 96 | .input-group-addon:first-child { 97 | border-right: 0; 98 | } 99 | .input-group .form-control:last-child, 100 | .input-group-addon:last-child, 101 | .input-group-btn:last-child > .btn, 102 | .input-group-btn:last-child > .dropdown-toggle, 103 | .input-group-btn:first-child > .btn:not(:first-child) { 104 | @include border-left-radius(0); 105 | } 106 | .input-group-addon:last-child { 107 | border-left: 0; 108 | } 109 | 110 | // Button input groups 111 | // ------------------------- 112 | .input-group-btn { 113 | position: relative; 114 | white-space: nowrap; 115 | } 116 | .input-group-btn > .btn { 117 | position: relative; 118 | // Jankily prevent input button groups from wrapping 119 | + .btn { 120 | margin-left: -4px; 121 | } 122 | // Bring the "active" button to the front 123 | &:hover, 124 | &:active { 125 | z-index: 2; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /static/css/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding: $jumbotron-padding; 8 | margin-bottom: $jumbotron-padding; 9 | font-size: ($font-size-base * 1.5); 10 | font-weight: 200; 11 | line-height: ($line-height-base * 1.5); 12 | color: $jumbotron-color; 13 | background-color: $jumbotron-bg; 14 | 15 | h1 { 16 | line-height: 1; 17 | color: $jumbotron-heading-color; 18 | } 19 | p { 20 | line-height: 1.4; 21 | } 22 | 23 | .container & { 24 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 25 | } 26 | 27 | @media screen and (min-width: $screen-tablet) { 28 | padding-top: ($jumbotron-padding * 1.6); 29 | padding-bottom: ($jumbotron-padding * 1.6); 30 | 31 | .container & { 32 | padding-left: ($jumbotron-padding * 2); 33 | padding-right: ($jumbotron-padding * 2); 34 | } 35 | 36 | h1 { 37 | font-size: ($font-size-base * 4.5); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /static/css/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // Add hover effects, but only for links 18 | &[href] { 19 | &:hover, 20 | &:focus { 21 | color: $label-link-hover-color; 22 | text-decoration: none; 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | // Empty labels collapse automatically (not available in IE8) 28 | &:empty { 29 | display: none; 30 | } 31 | } 32 | 33 | // Colors 34 | // Contextual variations (linked labels get darker on :hover) 35 | 36 | .label-default { 37 | @include label-variant($label-default-bg); 38 | } 39 | 40 | .label-primary { 41 | @include label-variant($label-primary-bg); 42 | } 43 | 44 | .label-success { 45 | @include label-variant($label-success-bg); 46 | } 47 | 48 | .label-info { 49 | @include label-variant($label-info-bg); 50 | } 51 | 52 | .label-warning { 53 | @include label-variant($label-warning-bg); 54 | } 55 | 56 | .label-danger { 57 | @include label-variant($label-danger-bg); 58 | } 59 | -------------------------------------------------------------------------------- /static/css/bootstrap/_list-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | // Base class 6 | // 7 | // Easily usable on