├── batter ├── core │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── views.py │ ├── tests.py │ └── models.py ├── batter │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── test.py │ │ ├── production.py │ │ ├── local.py │ │ └── base.py │ ├── test.py │ ├── fixtures │ │ ├── test_user.json │ │ └── initial_data.json │ ├── urls.py │ ├── middleware.py │ └── wsgi.py ├── music │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_models.py │ │ ├── test_search_indexes.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto__add_musicupload.py │ │ ├── 0001_initial.py │ │ ├── 0004_auto__del_field_master_label.py │ │ └── 0003_auto__add_label__add_field_artist_image__add_field_artist_summary__add.py │ ├── views │ │ ├── __init__.py │ │ ├── music.py │ │ ├── search.py │ │ ├── generic.py │ │ └── upload.py │ ├── admin.py │ ├── search_indexes.py │ ├── urls.py │ ├── forms.py │ ├── types.py │ └── models.py ├── profiles │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_models.py │ ├── views │ │ └── __init__.py │ ├── admin.py │ └── models.py ├── torrents │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── archlinux-2013.04.01-dual.iso.torrent │ │ ├── local_settings.py │ │ ├── test_fields.py │ │ ├── test_views.py │ │ └── test_models.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── forms.py │ ├── urls.py │ ├── fields.py │ ├── views.py │ └── models.py ├── notifications │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_context_processors.py │ │ ├── test_backend.py │ │ ├── test_models.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto__add_field_notification_seen.py │ │ ├── 0004_auto__del_field_notification_seen.py │ │ ├── 0001_initial.py │ │ └── 0003_auto__add_field_notification_title_text__add_field_notification_body_t.py │ ├── urls.py │ ├── context_processors.py │ ├── backend.py │ ├── views.py │ └── models.py ├── static │ ├── js │ │ └── project.js │ ├── img │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png │ └── css │ │ ├── project.css │ │ └── bootstrap-responsive.min.css ├── templates │ ├── search │ │ ├── indexes │ │ │ └── music │ │ │ │ ├── artist_text.txt │ │ │ │ └── master_text.txt │ │ ├── results │ │ │ ├── search_result.haml │ │ │ ├── artist_result.haml │ │ │ ├── search_result_base.haml │ │ │ └── master_result.haml │ │ └── search.haml │ ├── index.haml │ ├── torrents │ │ ├── torrent_detail.haml │ │ └── upload.haml │ ├── music │ │ ├── upload │ │ │ ├── release.html │ │ │ └── base.html │ │ ├── artist_detail.haml │ │ └── master_detail.haml │ ├── _messages.html │ ├── pagination │ │ ├── pagination.html │ │ ├── builtin_pagination.html │ │ └── django_pagination_pagination.html │ ├── 404.haml │ ├── 500.haml │ ├── notifications │ │ └── list.html │ └── base.haml └── manage.py ├── vagrant ├── puppet │ ├── modules │ │ └── .gitignore │ └── manifests │ │ └── default.pp ├── files │ ├── install_venv.sh │ ├── bash_aliases │ └── tmux.conf ├── README.md ├── Vagrantfile └── Vagrantfile.old ├── docs ├── __init__.py ├── deploy.rst ├── install.rst ├── index.rst ├── make.bat ├── Makefile └── conf.py ├── requirements.txt ├── requirements ├── local.txt ├── test.txt ├── production.txt └── base.txt ├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.txt └── README.md /batter/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/batter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/music/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/torrents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/music/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/profiles/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/profiles/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/torrents/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vagrant/puppet/modules/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/batter/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/music/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/notifications/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/torrents/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/notifications/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /batter/core/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /batter/profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | -------------------------------------------------------------------------------- /batter/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ -------------------------------------------------------------------------------- /batter/templates/search/indexes/music/artist_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.name }} 2 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory -------------------------------------------------------------------------------- /batter/templates/index.haml: -------------------------------------------------------------------------------- 1 | - extends "base.haml" 2 | 3 | - block title 4 | Home 5 | 6 | - block content 7 | -------------------------------------------------------------------------------- /batter/templates/torrents/torrent_detail.haml: -------------------------------------------------------------------------------- 1 | - extends "base.html" 2 | 3 | - block content 4 | = object 5 | -------------------------------------------------------------------------------- /docs/deploy.rst: -------------------------------------------------------------------------------- 1 | Deploy 2 | ======== 3 | 4 | This is where you describe how the project is deployed in production. -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ========= 3 | 4 | This is where you write how to get a new laptop to run this project. -------------------------------------------------------------------------------- /batter/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wafflesfm/batter/HEAD/batter/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /batter/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wafflesfm/batter/HEAD/batter/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /batter/torrents/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from torrents.models import Torrent 4 | 5 | admin.site.register(Torrent) 6 | -------------------------------------------------------------------------------- /batter/music/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .generic import EnforcingSlugDetailView 2 | from .music import ArtistView, MasterView 3 | from .search import SearchView 4 | -------------------------------------------------------------------------------- /batter/templates/search/indexes/music/master_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.name }} 2 | 3 | {% for artist in object.artists.all %} 4 | {{ artist.name }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /batter/templates/music/upload/release.html: -------------------------------------------------------------------------------- 1 | {% extends "music/upload/base.html" %} 2 | 3 | {% block context %} 4 | torrent name: {{ torrent_name }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /batter/torrents/tests/archlinux-2013.04.01-dual.iso.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wafflesfm/batter/HEAD/batter/torrents/tests/archlinux-2013.04.01-dual.iso.torrent -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is here because many Platforms as a Service look for 2 | # requirements.txt in the root directory of a project. 3 | -r requirements/production.txt -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | # Local development dependencies go here 2 | -r base.txt 3 | coverage==3.6 4 | django-discover-runner==1.0 5 | django-debug-toolbar==0.9.4 6 | Sphinx==1.1.3 -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # Test dependencies go here. 2 | -r base.txt 3 | coverage==3.6 4 | django-discover-runner==0.2.2 5 | pep8==1.4.5 6 | pyflakes==0.6.1 7 | flake8==2.0 8 | -------------------------------------------------------------------------------- /batter/templates/search/results/search_result.haml: -------------------------------------------------------------------------------- 1 | - with template_name=result.model_name|stringformat:"s"|add:"_result.haml" 2 | - include "search/results/"|add:template_name 3 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # Pro-tip: Try not to put anything here. There should be no dependency in 2 | # production that isn't in development. 3 | -r base.txt 4 | 5 | gunicorn==0.17.0 6 | -------------------------------------------------------------------------------- /batter/torrents/tests/local_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os.path 4 | 5 | TEST_FILE_PATH = os.path.join( 6 | os.path.dirname(os.path.abspath(__file__)), 7 | 'archlinux-2013.04.01-dual.iso.torrent') 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = batter 3 | # try to omit migrations 4 | omit = batter/*/migrations/*,batter/batter/settings/*,batter/batter/wsgi* 5 | 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | def __unicode__ 10 | def __str__ 11 | -------------------------------------------------------------------------------- /batter/music/views/music.py: -------------------------------------------------------------------------------- 1 | from ..models import Artist, Master 2 | from .generic import EnforcingSlugDetailView 3 | 4 | 5 | class ArtistView(EnforcingSlugDetailView): 6 | model = Artist 7 | 8 | 9 | class MasterView(EnforcingSlugDetailView): 10 | model = Master 11 | -------------------------------------------------------------------------------- /batter/templates/_messages.html: -------------------------------------------------------------------------------- 1 | {% for message in messages %} 2 |
3 | 4 | {{ message }} 5 |
6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /batter/templates/pagination/pagination.html: -------------------------------------------------------------------------------- 1 | {# Includes django-pagination template #} 2 | {# If you want to use django builtin pagination override this template #} 3 | {# and include ``pagination/django_pagination_pagination.html`` instead. #} 4 | {% include "pagination/django_pagination_pagination.html" %} 5 | -------------------------------------------------------------------------------- /batter/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "batter.settings.local") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /batter/music/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from music.models import Artist, Label, Master, MusicUpload, Release 4 | 5 | 6 | admin.site.register(Artist) 7 | admin.site.register(Label) 8 | admin.site.register(Master) 9 | admin.site.register(MusicUpload) 10 | admin.site.register(Release) 11 | -------------------------------------------------------------------------------- /batter/torrents/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django import forms 4 | from django.utils.translation import ugettext as _ 5 | 6 | from .fields import TorrentField 7 | 8 | 9 | class TorrentUploadForm(forms.Form): 10 | torrent_file = TorrentField(label=_("torrent file")) 11 | -------------------------------------------------------------------------------- /batter/templates/404.haml: -------------------------------------------------------------------------------- 1 | - extends "base.html" 2 | - load i18n 3 | 4 | - block title 5 | - trans "Not Found" 6 | 7 | - block content 8 | %header#overview.jumbotron.subhead 9 | %h1 10 | - trans "Page not found" 11 | %p.lead 12 | - trans "We're sorry but that page could not be found." 13 | -------------------------------------------------------------------------------- /batter/templates/music/artist_detail.haml: -------------------------------------------------------------------------------- 1 | - extends "base.haml" 2 | 3 | - block title 4 | = artist.name 5 | 6 | - block content 7 | %h1= artist.name 8 | %b Masters 9 | %ul 10 | - for master in artist.master_set.all 11 | %li 12 | {{ master.name }} 13 | -------------------------------------------------------------------------------- /vagrant/files/install_venv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /usr/local/bin/virtualenvwrapper.sh 4 | 5 | venv=batter 6 | 7 | # mkvirtualenv $venv > /dev/null 8 | # sleep 3 9 | # workon $venv > /dev/null 10 | # sleep 3 11 | # /home/vagrant/.virtualenvs/$venv/bin/pip install -r /home/vagrant/batter/requirements.txt > /dev/null 12 | 13 | exit -------------------------------------------------------------------------------- /batter/notifications/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.views.generic import TemplateView 3 | 4 | from . import views 5 | 6 | urlpatterns = patterns( 7 | '', 8 | url( 9 | r'^list/$', 10 | views.NotificationList.as_view(), 11 | name="notifications_list" 12 | ), 13 | ) 14 | -------------------------------------------------------------------------------- /batter/templates/search/results/artist_result.haml: -------------------------------------------------------------------------------- 1 | - extends "search/results/search_result_base.haml" 2 | - load static 3 | - load i18n 4 | 5 | - block result-logo 6 | %img.media-object{src: 'http://placehold.it/64', width: 64} 7 | 8 | - block result-name 9 | = result.object.name 10 | 11 | - block result-body 12 | %p 13 | - trans "Artist" 14 | -------------------------------------------------------------------------------- /batter/templates/search/results/search_result_base.haml: -------------------------------------------------------------------------------- 1 | .media 2 | %a.pull-left{href: '{{ result.object.get_absolute_url }}'} 3 | - block result-logo 4 | .media-body 5 | %h4.media-heading 6 | -block result-name-base 7 | %a{href: '{{ result.object.get_absolute_url }}'} 8 | - block result-name 9 | - block result-body 10 | -------------------------------------------------------------------------------- /vagrant/files/bash_aliases: -------------------------------------------------------------------------------- 1 | export PATH 2 | 3 | export WORKON_HOME=$HOME/.virtualenvs 4 | source /usr/local/bin/virtualenvwrapper.sh 5 | 6 | mkvirtualenv batter 7 | echo "CHECKING PYTHON PACKAGES" 8 | /home/vagrant/.virtualenvs/batter/bin/pip install -r /home/vagrant/batter/requirements/local.txt 9 | echo "FINISHED" 10 | echo "Code is located in ~/batter. Have Fun!" -------------------------------------------------------------------------------- /batter/templates/500.haml: -------------------------------------------------------------------------------- 1 | - extends "base.html" 2 | - load i18n 3 | 4 | - block title 5 | - trans "Server Error" 6 | 7 | - block content 8 | %header#overview.jumbotron.subhead 9 | %h1 10 | - trans "Something went wrong" 11 | %p.lead 12 | - trans "We're sorry but a server error has occurred. We've been notified and will look into it as soon as possible." 13 | -------------------------------------------------------------------------------- /batter/templates/torrents/upload.haml: -------------------------------------------------------------------------------- 1 | - extends "base.html" 2 | - load bootstrap_tags 3 | 4 | - block content 5 | %form{action: '{% url "torrents_torrent_upload" %}', method: 'post', enctype: 'multipart/form-data'} 6 | %legend Upload Torrent 7 | - csrf_token 8 | = form|as_bootstrap 9 | .form-actions 10 | %button.btn.btn-primary{type: 'submit', value: 'Upload'} Upload 11 | -------------------------------------------------------------------------------- /batter/music/views/search.py: -------------------------------------------------------------------------------- 1 | from haystack.query import SearchQuerySet 2 | from haystack.views import FacetedSearchView 3 | 4 | 5 | class SearchView(FacetedSearchView): 6 | def __init__(self, *args, **kwargs): 7 | sqs = SearchQuerySet() 8 | kwargs.update({ 9 | 'searchqueryset': sqs 10 | }) 11 | super(SearchView, self).__init__(*args, **kwargs) 12 | -------------------------------------------------------------------------------- /batter/batter/test.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class LoggedInTestCase(TestCase): 6 | def setUp(self): 7 | self.user = User.objects.create_user( 8 | 'samantha', 9 | 'samantha@example.com', 10 | 'soliloquy' 11 | ) 12 | self.client.login(username='samantha', password='soliloquy') 13 | -------------------------------------------------------------------------------- /batter/core/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /batter/torrents/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from .views import DownloadView, TorrentView 4 | 5 | urlpatterns = patterns( 6 | '', 7 | url(r'(?P\d+)/$', TorrentView.as_view(), 8 | name="torrents_torrent_view"), 9 | url(r'upload/$', "torrents.views.upload_torrent", 10 | name="torrents_torrent_upload"), 11 | url(r'(?P\d+)/download/$', DownloadView.as_view(), 12 | name="torrents_torrent_download"), 13 | ) 14 | -------------------------------------------------------------------------------- /vagrant/README.md: -------------------------------------------------------------------------------- 1 | Using Vagrant for development of Batter 2 | ======================================= 3 | 4 | 1. Install vagrant (http://vagrantup.com) 5 | 2. In this directory run `vagrant up` 6 | 3. Run `vagrant ssh` 7 | 4. Once you are ssh'd go ahead and run `workon batter` to activate the `venv` 8 | 5. You're good to go! `vagrant` automatically installed all the required python libs and system packages and mounted the projects code folder to a network share called `batter`. Happy Dev'ing! -------------------------------------------------------------------------------- /batter/core/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.utils.translation import ugettext as _ 4 | from userena.models import UserenaLanguageBaseProfile 5 | 6 | 7 | class UserProfile(UserenaLanguageBaseProfile): 8 | user = models.OneToOneField(User, 9 | unique=True, 10 | verbose_name=_('user'), 11 | related_name='user_profile') 12 | -------------------------------------------------------------------------------- /batter/templates/search/results/master_result.haml: -------------------------------------------------------------------------------- 1 | - extends "search/results/search_result_base.haml" 2 | - load static 3 | 4 | - block result-logo 5 | %img.media-object{src: 'http://placehold.it/64', width: 64} 6 | 7 | - block result-name 8 | = result.object.name 9 | 10 | - block result-body 11 | - for artist in result.object.artists.all 12 | {{ artist.name }}{% if not forloop.last %}, {% endif %} 13 | %p= result.object.modified 14 | -------------------------------------------------------------------------------- /batter/torrents/fields.py: -------------------------------------------------------------------------------- 1 | from BTL import BTFailure 2 | from django import forms 3 | from django.core.exceptions import ValidationError 4 | 5 | from .models import Torrent 6 | 7 | 8 | class TorrentField(forms.FileField): 9 | def to_python(self, data): 10 | data = super(TorrentField, self).to_python(data) 11 | if data is None: 12 | raise ValidationError(self.error_messages['empty']) 13 | 14 | try: 15 | return Torrent.from_torrent_file(data) 16 | except BTFailure as e: 17 | raise ValidationError(str(e)) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode: 2 | *.py[co] 3 | 4 | # Packaging files: 5 | *.egg* 6 | 7 | # Editor temp files: 8 | *.swp 9 | *~ 10 | 11 | # Sphinx docs: 12 | build 13 | 14 | # SQLite3 database files: 15 | *.db 16 | 17 | # Logs: 18 | *.log 19 | 20 | # Virtual environment: 21 | venv 22 | 23 | #Vagrant metadata: 24 | .vagrant 25 | *.sha1 26 | .vagrant.* 27 | 28 | #torrents 29 | *.torrent 30 | 31 | # Coverage 32 | .coverage 33 | htmlcov/ 34 | 35 | #pyCharm 36 | .idea/ 37 | src/ 38 | /.project 39 | /.pydevproject 40 | /.settings/org.eclipse.core.resources.prefs 41 | /batter/media/ 42 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. batter documentation master file, created by 2 | sphinx-quickstart on Sun Feb 17 11:46:20 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to batter's documentation! 7 | ==================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | install 15 | deploy 16 | tests 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | BitTorrent-bencode==5.0.8.1 2 | Django==1.5.1 3 | Jinja2==2.7 4 | Pillow==2.1.0 5 | South==0.8.1 6 | bpython==0.12 7 | django-braces==1.0.0 8 | django-extensions==1.1.1 9 | django-forms-bootstrap==2.0.3.post1 10 | django-grappelli==2.4.5 11 | django-guardian==1.1.1 12 | django-haystack==2.0.0 13 | django-model-utils==1.4.0 14 | django-notification==1.1 15 | django-userena==1.2.1 16 | django-widget-tweaks==1.3 17 | easy-thumbnails==1.3 18 | hamlpy==0.82.2 19 | jsonfield==0.9.17 20 | logutils==0.3.3 21 | pyelasticsearch==0.5 22 | requests==1.2.3 23 | simplejson==3.3.0 24 | -------------------------------------------------------------------------------- /batter/notifications/context_processors.py: -------------------------------------------------------------------------------- 1 | from . import models 2 | 3 | 4 | def notifications(request): 5 | """ 6 | Context processor for notifications 7 | 8 | This is required because I don't want to override Django's 9 | RelatedManager, so it's easier to attack this problem in reverse. 10 | """ 11 | user = request.user 12 | if user.is_authenticated(): 13 | notifications = models.Notification.objects.by_user(user).unseen() 14 | return { 15 | 'unseen_notifications': notifications 16 | } 17 | else: 18 | return {} 19 | -------------------------------------------------------------------------------- /batter/batter/fixtures/test_user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "vagrant", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2013-04-13T20:16:52.777Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$ofgBqYKGyeIg$PJCpF79E++n0/Z7HgM46Qv9CLd1IcAhsfppwNfmV/aU=", 16 | "email": "a@a.com", 17 | "date_joined": "2013-04-13T20:16:52.777Z" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /batter/templates/music/master_detail.haml: -------------------------------------------------------------------------------- 1 | - extends "base.haml" 2 | 3 | - block title 4 | = master.name 5 | 6 | - block content 7 | %h1= master.main.name 8 | %b Artist{{ master.artists.count|pluralize }} 9 | %ul 10 | - for artist in master.artists.all 11 | %li 12 | {{ artist.name }} 13 | %b Formats 14 | %ul 15 | - for release in master.release_set.all 16 | %li 17 | %b {{ master.name }} - {{ release.name }} 18 | %ul 19 | - for mu in release.musicupload_set.all 20 | %li 21 | {{ mu }} 22 | -------------------------------------------------------------------------------- /batter/music/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import hashlib 4 | 5 | from django.test import TestCase 6 | 7 | from ..models import Artist, Master, Release 8 | 9 | 10 | class ArtistTests(TestCase): 11 | def test_absolute_url(self): 12 | # Just poke it 13 | artist = Artist(name="Okkervil River", slug="Okkervil-River") 14 | artist.save() 15 | artist.get_absolute_url() 16 | 17 | 18 | class MasterTests(TestCase): 19 | def test_absolute_url(self): 20 | # Just poke it 21 | master = Master(name="Black Sheep Boy", slug="Black-Sheep-Boy") 22 | master.save() 23 | master.get_absolute_url() 24 | -------------------------------------------------------------------------------- /batter/music/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | 3 | from .models import Artist, Master 4 | 5 | 6 | class ArtistIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.EdgeNgramField(document=True, use_template=True) 8 | 9 | def get_model(self): 10 | return Artist 11 | 12 | def index_queryset(self, using=None): 13 | return self.get_model().objects.all() 14 | 15 | 16 | class MasterIndex(indexes.SearchIndex, indexes.Indexable): 17 | text = indexes.EdgeNgramField(document=True, use_template=True) 18 | 19 | def get_model(self): 20 | return Master 21 | 22 | def index_queryset(self, using=None): 23 | return self.get_model().objects.all() 24 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "precise64" 6 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 7 | config.vm.network :public_network 8 | config.vm.network :forwarded_port, guest: 8000, host: 8080 9 | config.vm.synced_folder "../", "/home/vagrant/batter" 10 | 11 | config.vm.provider :virtualbox do |vb| 12 | vb.customize ["modifyvm", :id, "--name", "batter"] 13 | end 14 | 15 | config.vm.provision :puppet do |puppet| 16 | puppet.manifests_path = "puppet/manifests" 17 | puppet.manifest_file = "default.pp" 18 | puppet.module_path = "puppet/modules" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile.old: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("1") do |config| 5 | config.vm.box = "precise64" 6 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 7 | config.vm.network :bridged 8 | config.vm.forward_port 8080, 8000 9 | config.vm.share_folder "batter", "/home/vagrant/batter", "../" 10 | config.vm.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/batter", "1"] 11 | config.vm.customize ["modifyvm", :id, "--name", "batter"] 12 | 13 | config.vm.provision :puppet do |puppet| 14 | puppet.manifests_path = "puppet/manifests" 15 | puppet.manifest_file = "default.pp" 16 | puppet.module_path = "puppet/modules" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /batter/templates/notifications/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_title %}Notifications{% endblock %} 4 | 5 | {% block body %} 6 |

Your Notifications

7 | {% include "pagination/pagination.html" %} 8 | {% if object_list %} 9 | 17 | {% else %} 18 |

You don't have any notifications yet.

19 | {% endif %} 20 | {% include "pagination/pagination.html" %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /batter/music/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from .views import SearchView, ArtistView, MasterView 4 | from .views.upload import MusicUploadWizard, FORMS, CONDITIONS 5 | 6 | urlpatterns = patterns( 7 | '', 8 | url(r'^search/$', 9 | SearchView(), 10 | name='music_search'), 11 | url(r'^upload/$', 12 | # TODO: use form_list (see MusicUploadWizard definition) 13 | MusicUploadWizard.as_view(FORMS, condition_dict=CONDITIONS), 14 | name="upload_music"), 15 | url(r'^(?P[-\w]+)-(?P\d+)/$', 16 | ArtistView.as_view(), 17 | name="music_artist_detail"), 18 | url(r'^album/(?P[-\w]+)-(?P\d+)/$', 19 | MasterView.as_view(), 20 | name="music_master_detail"), 21 | ) 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - wget -O /home/travis/cache.tar.gz https://github.com/senturio/batter-packages-cache/archive/master.tar.gz 6 | - tar -xvzf /home/travis/cache.tar.gz 7 | - bash batter-packages-cache-master/install_these.sh 8 | # - pip install -r requirements/test.txt --index-url=https://simple.crate.io 9 | - pip install coveralls --index-url=https://simple.crate.io 10 | script: 11 | - coverage run batter/manage.py test --settings=batter.settings.test 12 | - flake8 --select=E,W batter --exclude="migrations" 13 | after_success: 14 | - coveralls 15 | notifications: 16 | webhooks: 17 | urls: 18 | - http://batterbetterbotter.herokuapp.com/hubot/travis-ci?room=%23batter 19 | on_success: always 20 | on_failure: always 21 | on_start: true 22 | -------------------------------------------------------------------------------- /batter/music/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django import forms 4 | from django.utils.translation import ugettext as _ 5 | 6 | from torrents.forms import TorrentUploadForm 7 | 8 | from .types import UPLOAD_TYPES, FORMAT_TYPES, BITRATE_TYPES, RELEASE_TYPES 9 | from .types import MEDIA_TYPES 10 | 11 | 12 | class TorrentTypeForm(TorrentUploadForm): 13 | type = forms.ChoiceField(UPLOAD_TYPES) 14 | 15 | 16 | class ReleaseInfoForm(forms.Form): 17 | artist = forms.CharField() 18 | album = forms.CharField() 19 | year = forms.CharField() 20 | 21 | 22 | class FileInfoForm(forms.Form): 23 | format = forms.ChoiceField(FORMAT_TYPES) 24 | bitrate = forms.ChoiceField(BITRATE_TYPES) 25 | release = forms.ChoiceField(RELEASE_TYPES) 26 | media = forms.ChoiceField(MEDIA_TYPES) 27 | -------------------------------------------------------------------------------- /batter/batter/settings/test.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | ########## TEST SETTINGS 4 | TEST_RUNNER = 'discover_runner.DiscoverRunner' 5 | TEST_DISCOVER_TOP_LEVEL = SITE_ROOT 6 | TEST_DISCOVER_ROOT = SITE_ROOT 7 | TEST_DISCOVER_PATTERN = "test_*.py" 8 | ########## IN-MEMORY TEST DATABASE 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | "NAME": ":memory:", 13 | "USER": "", 14 | "PASSWORD": "", 15 | "HOST": "", 16 | "PORT": "", 17 | }, 18 | } 19 | 20 | ########## HAYSTACK SEARCH CONFIGURATION 21 | # "Simple" backend to avoid configuration for tests. 22 | HAYSTACK_CONNECTIONS = { 23 | 'default': { 24 | 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', 25 | }, 26 | } 27 | ########## END HAYSTACK SEARCH CONFIGURATION 28 | -------------------------------------------------------------------------------- /batter/batter/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "sites.site", 4 | "pk": 1, 5 | "fields": { 6 | "domain": "localhost", 7 | "name": "Batter" 8 | } 9 | }, 10 | { 11 | "model": "auth.user", 12 | "pk": 1, 13 | "fields": { 14 | "username": "vagrant", 15 | "first_name": "", 16 | "last_name": "", 17 | "is_active": true, 18 | "is_superuser": true, 19 | "is_staff": true, 20 | "last_login": "2013-04-13T20:16:52.777Z", 21 | "groups": [], 22 | "user_permissions": [], 23 | "password": "pbkdf2_sha256$10000$ofgBqYKGyeIg$PJCpF79E++n0/Z7HgM46Qv9CLd1IcAhsfppwNfmV/aU=", 24 | "email": "a@a.com", 25 | "date_joined": "2013-04-13T20:16:52.777Z" 26 | } 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /batter/profiles/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | 4 | from .. import models 5 | 6 | 7 | class ProfileTests(TestCase): 8 | def setUp(self): 9 | self.samantha = User.objects.create_user( 10 | 'samantha', 11 | 'samantha@example.com', 12 | 'soliloquy' 13 | ) 14 | self.profile = models.Profile(user=self.samantha) 15 | 16 | def test_trackerid_generation(self): 17 | profile = self.profile 18 | self.assertIsNone(profile.trackerid) 19 | profile.save() 20 | self.assertEquals(len(profile.trackerid), 32) 21 | 22 | def test_unicode(self): 23 | self.assertEquals(unicode(self.profile), "samantha") 24 | 25 | 26 | class HelperTests(TestCase): 27 | def test_generate_trackerid(self): 28 | trackerid = models.generate_trackerid() 29 | self.assertEquals(len(trackerid), 32) 30 | -------------------------------------------------------------------------------- /vagrant/files/tmux.conf: -------------------------------------------------------------------------------- 1 | # Change prefix key to Ctrl+a 2 | unbind C-b 3 | set -g prefix C-a 4 | 5 | # Last active window 6 | unbind l 7 | bind C-a last-window 8 | 9 | # More straight forward key bindings for splitting 10 | unbind % 11 | bind | split-window -h 12 | bind h split-window -h 13 | unbind '"' 14 | bind - split-window -v 15 | bind v split-window -v 16 | 17 | # History 18 | set -g history-limit 1000 19 | 20 | # Terminal emulator window title 21 | set -g set-titles on 22 | set -g set-titles-string '#S:#I.#P #W' 23 | 24 | # Status Bar 25 | set -g status-bg black 26 | set -g status-fg white 27 | set -g status-interval 1 28 | set -g status-left '#[fg=green]#H#[default]' 29 | set -g status-right '#[default] #[fg=cyan,bold]%Y-%m-%d %H:%M:%S#[default]' 30 | 31 | # Notifying if other windows has activities 32 | setw -g monitor-activity on 33 | set -g visual-activity on 34 | 35 | # Clock 36 | setw -g clock-mode-colour green 37 | setw -g clock-mode-style 24 38 | 39 | set -g default-terminal "screen-256color" -------------------------------------------------------------------------------- /batter/music/views/generic.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import resolve 2 | from django.shortcuts import redirect 3 | from django.views.generic.detail import DetailView 4 | 5 | 6 | class EnforcingSlugDetailView(DetailView): 7 | """ 8 | A DetailView that looks up by pk but enforces a valid slug in the url. 9 | """ 10 | def dispatch(self, request, *args, **kwargs): 11 | self.object = self.get_object() 12 | slug = self.kwargs.get(self.slug_url_kwarg, None) 13 | current_url = resolve(request.path_info).url_name 14 | 15 | if self.get_object().slug != slug: 16 | return redirect(current_url, 17 | pk=self.object.pk, 18 | slug=self.object.slug, 19 | permanent=True) 20 | 21 | return super(EnforcingSlugDetailView, self).dispatch(request, 22 | *args, 23 | **kwargs) 24 | -------------------------------------------------------------------------------- /batter/profiles/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.contrib.auth.models import User 4 | from django.db import models 5 | 6 | 7 | class Profile(models.Model): 8 | user = models.ForeignKey(User, unique=True) 9 | trackerid = models.CharField(max_length=32, blank=True, null=True) 10 | 11 | def save(self, *args, **kwargs): 12 | """ 13 | override save method to generate a trackerid 14 | for torrent tracker url generation 15 | """ 16 | 17 | if not self.trackerid: 18 | self.trackerid = generate_trackerid() 19 | super(Profile, self).save(*args, **kwargs) 20 | 21 | def __unicode__(self): 22 | return self.user.username 23 | 24 | 25 | # helpers 26 | def generate_trackerid(): 27 | """ 28 | generate a uuid and check if it already exists in a profile 29 | """ 30 | 31 | trackerid = None 32 | while trackerid is None or \ 33 | Profile.objects.filter(trackerid=trackerid).exists(): 34 | trackerid = uuid.uuid4().hex 35 | return trackerid 36 | -------------------------------------------------------------------------------- /batter/batter/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import patterns, include, url 3 | from django.conf.urls.static import static 4 | from django.views.generic import TemplateView 5 | 6 | # Uncomment the next two lines to enable the admin: 7 | from django.contrib import admin 8 | admin.autodiscover() 9 | 10 | urlpatterns = patterns( 11 | '', 12 | url(r'^$', TemplateView.as_view(template_name='index.html'), name="home"), 13 | 14 | # Examples: 15 | # url(r'^$', 'batter.views.home', name='home'), 16 | # url(r'^batter/', include('batter.foo.urls')), 17 | url(r'^accounts/', include('userena.urls')), 18 | url(r"^notifications/", include("notifications.urls")), 19 | url(r'^torrents/', include("torrents.urls")), 20 | url(r'^music/', include("music.urls")), 21 | 22 | url(r'^admin/', include(admin.site.urls)), 23 | url(r'^grappelli/', include('grappelli.urls')), 24 | ) 25 | 26 | if settings.DEBUG: 27 | urlpatterns += static(settings.MEDIA_URL, 28 | document_root=settings.MEDIA_ROOT) 29 | -------------------------------------------------------------------------------- /batter/templates/music/upload/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head_title %} 5 | Upload 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}

10 | 11 | {% block context %}{% endblock %} 12 | 13 |
{% csrf_token %} 14 |
15 | {{ wizard.management_form.as_p }} 16 | {% if wizard.form.forms %} 17 | {{ wizard.form.management_form.as_p }} 18 | {% for form in wizard.form.forms %} 19 | {{ form.as_p }} 20 | {% endfor %} 21 | {% else %} 22 | {{ wizard.form.as_p }} 23 | {% endif %} 24 | {% if wizard.steps.prev %} 25 | 26 | 27 | {% endif %} 28 | 29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /batter/notifications/backend.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext 2 | 3 | from notification import backends 4 | 5 | from . import models 6 | 7 | 8 | class ModelBackend(backends.BaseBackend): 9 | spam_sensitivity = 1 10 | 11 | def deliver(self, recipient, sender, notice_type, extra_context): 12 | context = self.default_context() 13 | context.update({ 14 | "recipient": recipient, 15 | "notice": ugettext(notice_type.display) 16 | }) 17 | context.update(extra_context) 18 | 19 | messages = self.get_formatted_messages(( 20 | "short.txt", 21 | "full.txt", 22 | "short.html", 23 | "full.html" 24 | ), notice_type.label, context) 25 | 26 | notification = models.Notification() 27 | notification.recipient = recipient 28 | 29 | notification.title = messages["short.html"] 30 | notification.body = messages["full.html"] 31 | notification.title_text = messages["short.txt"] 32 | notification.body_text = messages["full.txt"] 33 | 34 | notification.save() 35 | -------------------------------------------------------------------------------- /batter/static/css/project.css: -------------------------------------------------------------------------------- 1 | /*! project specific CSS goes here. */ 2 | 3 | /* ugly copy-pasta to make form-included links work in dropdowns */ 4 | .dropdown-menu > form > li > a { 5 | display: block; 6 | padding: 3px 20px; 7 | clear: both; 8 | font-weight: normal; 9 | line-height: 20px; 10 | color: #333333; 11 | white-space: nowrap; 12 | } 13 | .dropdown-menu > form > li > a:hover, .dropdown-menu > form > li > a:focus { 14 | color: #ffffff; 15 | text-decoration: none; 16 | background-color: #0081c2; 17 | background-image: -moz-linear-gradient(top, #0088cc, #0077b3); 18 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); 19 | background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); 20 | background-image: -o-linear-gradient(top, #0088cc, #0077b3); 21 | background-image: linear-gradient(to bottom, #0088cc, #0077b3); 22 | background-repeat: repeat-x; 23 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); 24 | } 25 | .dropdown-menu > form > li > a:hover > i[class^="icon-"] { 26 | background-image: url("../img/glyphicons-halflings-white.png"); 27 | } -------------------------------------------------------------------------------- /batter/batter/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.conf import settings 3 | from re import compile 4 | 5 | 6 | EXEMPT_URLS = [compile(settings.LOGIN_URL.lstrip('/'))] 7 | if hasattr(settings, 'LOGIN_EXEMPT_URLS'): 8 | EXEMPT_URLS += [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS] 9 | 10 | 11 | class LoginRequiredMiddleware(object): 12 | """ 13 | Middleware that requires a user to be authenticated to view any page other 14 | than LOGIN_URL. Exemptions to this requirement can optionally be specified 15 | in settings via a list of regular expressions in LOGIN_EXEMPT_URLS (which 16 | you can copy from your urls.py). 17 | 18 | Requires authentication middleware and template context processors to be 19 | loaded. You'll get an error if they aren't. 20 | """ 21 | def process_request(self, request): 22 | assert hasattr(request, 'user') 23 | if not request.user.is_authenticated(): 24 | path = request.path_info.lstrip('/') 25 | if not any(m.match(path) for m in EXEMPT_URLS): 26 | return HttpResponseRedirect(settings.LOGIN_URL) 27 | -------------------------------------------------------------------------------- /batter/templates/pagination/builtin_pagination.html: -------------------------------------------------------------------------------- 1 | {# Pagination for default django.core.paginator.Paginator #} 2 | {# This template will work with CBV views with ``paginate_by`` specified. #} 3 | {% load i18n %} 4 | 5 | {% if is_paginated %} 6 | 25 | {% endif %} 26 | -------------------------------------------------------------------------------- /batter/torrents/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from cStringIO import StringIO 4 | 5 | from django.test import TestCase 6 | from django.core.exceptions import ValidationError 7 | from django.core.files import File 8 | 9 | from .local_settings import TEST_FILE_PATH 10 | from ..fields import TorrentField 11 | 12 | 13 | class TorrentFieldTests(TestCase): 14 | def test_empty(self): 15 | field = TorrentField() 16 | self.assertRaises(ValidationError, field.clean, False) 17 | 18 | def test_creates_torrent(self): 19 | torrent_file_raw = open(TEST_FILE_PATH, 'rb') 20 | torrent_data = torrent_file_raw.read() 21 | torrent_file_raw.seek(0) 22 | 23 | torrent_file = File(torrent_file_raw) 24 | field = TorrentField() 25 | torrent = field.clean(torrent_file) 26 | 27 | self.assertEquals(torrent_data, torrent.as_bencoded_string()) 28 | 29 | def test_invalid_torrent(self): 30 | field = TorrentField() 31 | not_a_torrent = File(StringIO("this is clearly an invalid torrent")) 32 | not_a_torrent.name = "invalid.torrent" 33 | self.assertRaises(ValidationError, field.clean, not_a_torrent) 34 | -------------------------------------------------------------------------------- /batter/templates/pagination/django_pagination_pagination.html: -------------------------------------------------------------------------------- 1 | {# Pagination for django-pagination #} 2 | {% load i18n %} 3 | 4 | {% if is_paginated %} 5 | 28 | {% endif %} 29 | 30 | -------------------------------------------------------------------------------- /batter/music/tests/test_search_indexes.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.test import TestCase 4 | 5 | from ..models import Artist, Master 6 | from ..search_indexes import ArtistIndex, MasterIndex 7 | 8 | 9 | class ArtistIndexTests(TestCase): 10 | def setUp(self): 11 | self.index = ArtistIndex() 12 | 13 | def test_get_model(self): 14 | self.assertEquals(self.index.get_model(), Artist) 15 | 16 | def test_index_queryset(self): 17 | # Querysets are covered by Django tests, so just make sure that the QS 18 | # model is the same as the index model. 19 | self.assertEquals(self.index.index_queryset().model, 20 | self.index.get_model()) 21 | 22 | 23 | class MasterIndexTests(TestCase): 24 | def setUp(self): 25 | self.index = MasterIndex() 26 | 27 | def test_get_model(self): 28 | self.assertEquals(self.index.get_model(), Master) 29 | 30 | def test_index_queryset(self): 31 | # Querysets are covered by Django tests, so just make sure that the QS 32 | # model is the same as the index model. 33 | self.assertEquals(self.index.index_queryset().model, 34 | self.index.get_model()) 35 | -------------------------------------------------------------------------------- /batter/templates/search/search.haml: -------------------------------------------------------------------------------- 1 | - extends "base.haml" 2 | - load widget_tweaks 3 | 4 | - block content 5 | %form.form-search{method:'get', action:'.'} 6 | .row 7 | .span7.offset2 8 | = form.non_field_errors 9 | = form.q.errors 10 | {% render_field form.q class+="span6 search-query" autocomplete="off" placeholder="Search for artists, albums, and more"%} 11 | %input.btn{type:'submit', value:'Search'} 12 | - if query 13 | .row 14 | .span7.offset2 15 | #results 16 | %h4 Showing {{ page.start_index }} – {{ page.end_index }} of {{ page.paginator.count }} results for "{{ query }}" 17 | - for result in page.object_list 18 | - include "search/results/search_result.haml" 19 | - empty 20 | %b No results found for "{{ query }}." 21 | - if page.has_previous or page.has_next 22 | %div 23 | - if page.has_previous 24 | %a{href:'?q={{ query }}&page={{ page.previous_page_number }}'} 25 | « Previous 26 | - if page.has_previous and page.has_next 27 | | 28 | - if page.has_next 29 | %a{href:'?q={{ query }}&page={{ page.next_page_number }}'} 30 | Next » 31 | -------------------------------------------------------------------------------- /batter/notifications/tests/test_context_processors.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.test import TestCase 5 | from django.contrib.auth.models import User 6 | 7 | from ..models import Notification 8 | 9 | 10 | class NotificationsContextProcessorTests(TestCase): 11 | def setUp(self): 12 | self.samantha = User.objects.create_user( 13 | 'samantha', 14 | 'samantha@example.com', 15 | 'soliloquy' 16 | ) 17 | self.samantha_mail, _ = Notification.objects.get_or_create( 18 | recipient=self.samantha, 19 | title='You\'ve got mail!', 20 | body='joe sent you a message', 21 | title_text='You\'ve got mail!', 22 | body_text='joe sent you a message' 23 | ) 24 | 25 | def test_authenticated(self): 26 | self.client.login(username='samantha', password='soliloquy') 27 | response = self.client.get('/') 28 | self.assertEquals(response.status_code, 200) 29 | self.assertIn('unseen_notifications', response.context) 30 | 31 | def test_unauthenticated(self): 32 | response = self.client.get('/', follow=True) 33 | self.assertEquals(response.status_code, 200) 34 | self.assertNotIn('unseen_notifications', response.context) 35 | -------------------------------------------------------------------------------- /batter/notifications/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.list import ListView 2 | 3 | from braces.views import LoginRequiredMixin, JSONResponseMixin, \ 4 | AjaxResponseMixin 5 | 6 | from . import models 7 | 8 | 9 | class NotificationList( 10 | LoginRequiredMixin, 11 | JSONResponseMixin, 12 | AjaxResponseMixin, 13 | ListView 14 | ): 15 | http_method_names = ['get'] # get only 16 | allow_empty = True 17 | template_name = "notifications/list.html" 18 | ajax_show_on_page = 10 19 | paginate_by = 20 20 | content_type = 'text/html' 21 | 22 | def get_queryset(self): 23 | return models.Notification.objects.by_user(self.request.user) 24 | 25 | def get_ajax(self, request): 26 | self.object_list = self.get_queryset() 27 | self.content_type = 'application/json' 28 | 29 | paginator, page, object_list, more_pages = self.paginate_queryset( 30 | self.object_list, 31 | self.ajax_show_on_page 32 | ) 33 | 34 | next_p = page.next_page_number() if page.has_next() else None 35 | prev_p = page.previous_page_number() if page.has_previous() else None 36 | object_list = [o.as_dict() for o in object_list] 37 | return self.render_json_response({ 38 | 'total': paginator.count, 39 | 'pages': { 40 | 'count': paginator.num_pages, 41 | 'next': next_p, 42 | 'previous': prev_p, 43 | }, 44 | 'results': object_list, 45 | }) 46 | -------------------------------------------------------------------------------- /batter/torrents/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import cStringIO as StringIO 4 | 5 | from django.http import HttpResponse 6 | from django.shortcuts import render, redirect 7 | from django.template.defaultfilters import slugify 8 | from django.views.generic.detail import DetailView 9 | 10 | from .forms import TorrentUploadForm 11 | from .models import Torrent 12 | 13 | 14 | def upload_torrent(request): 15 | form = TorrentUploadForm(request.POST or None, request.FILES or None) 16 | if form.is_valid(): 17 | torrent = form.cleaned_data['torrent_file'] 18 | try: 19 | torrent.save() 20 | return redirect(torrent) 21 | except Exception: 22 | resp = HttpResponse() 23 | resp.status_code = 409 24 | return resp 25 | 26 | return render(request, 'torrents/upload.html', {'form': form}) 27 | 28 | 29 | class TorrentView(DetailView): 30 | model = Torrent 31 | 32 | 33 | class DownloadView(DetailView): 34 | model = Torrent 35 | 36 | def get(self, request, *args, **kwargs): 37 | torrent = self.get_object() 38 | torrent_file = StringIO.StringIO(torrent.as_bencoded_string()) 39 | 40 | response = HttpResponse( 41 | torrent_file.read(), content_type='application/x-bittorrent') 42 | response['Content-Length'] = torrent_file.tell() 43 | response['Content-Disposition'] = \ 44 | 'attachment; filename={0}.torrent'.format(slugify(torrent.name)) 45 | return response 46 | -------------------------------------------------------------------------------- /batter/notifications/tests/test_backend.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | 4 | from notification.models import NoticeType 5 | 6 | from ..backend import ModelBackend 7 | from ..models import Notification 8 | 9 | 10 | class StubbedModelBackend(ModelBackend): 11 | def get_formatted_messages(self, templates, label, context): 12 | return dict(zip(templates, ['message'] * len(templates))) 13 | 14 | 15 | class ModelBackendTests(TestCase): 16 | def setUp(self): 17 | self.backend = StubbedModelBackend('stubmodel') 18 | self.samantha = User.objects.create_user( 19 | 'samantha', 20 | 'samantha@example.com', 21 | 'soliloquy' 22 | ) 23 | self.new_message, _ = NoticeType.objects.get_or_create( 24 | label='mail', 25 | display='New Private Message', 26 | description='Notification when you receive a private message', 27 | default=1 28 | ) 29 | 30 | def test_deliver(self): 31 | self.backend.deliver( 32 | recipient=self.samantha, 33 | sender=None, 34 | notice_type=self.new_message, 35 | extra_context={} 36 | ) 37 | results = Notification.objects.by_user(self.samantha).unseen() 38 | self.assertEquals(len(results), 1) 39 | 40 | notification = results.get() 41 | self.assertEquals(notification.title, 'message') 42 | self.assertEquals(notification.body, 'message') 43 | self.assertEquals(notification.title_text, 'message') 44 | self.assertEquals(notification.body_text, 'message') 45 | -------------------------------------------------------------------------------- /batter/music/types.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext as _ 2 | 3 | UPLOAD_TYPES = ( 4 | ('music', _('Music')), 5 | ('applications', _('Applications')), 6 | ('ebooks', _('E-Books')), 7 | ('audiobooks', _('Audiobooks')), 8 | ('comedy', _('Comedy / Spoken Word')), 9 | ('comics', _('Comics')), 10 | ) 11 | 12 | FORMAT_TYPES = ( 13 | ('mp3', 'MP3'), 14 | ('flac', 'FLAC'), 15 | ('aac', 'AAC'), 16 | ('ac3', 'AC3'), 17 | ('dts', 'DTS'), 18 | ) 19 | 20 | BITRATE_TYPES = ( 21 | ('192', '192'), 22 | ('apsvbr', 'APS (VBR)'), 23 | ('v2vbr', 'V2 (VBR)'), 24 | ('v1vbr', 'V1 (VBR)'), 25 | ('256', '256'), 26 | ('apxvbr', 'APX (VBR)'), 27 | ('v0vbr', 'V0 (VBR)'), 28 | ('320', '320'), 29 | ('lossless', _('Lossless')), 30 | ('24bitlossless', _('24Bit Lossless')), 31 | ('v8vbr', 'V8 (VBR)'), 32 | ('other', _('Other')), 33 | ) 34 | 35 | MEDIA_TYPES = ( 36 | ('cd', 'CD'), 37 | ('dvd', 'DVD'), 38 | ('vinyl', _('Vinyl')), 39 | ('soundboard', _('Soundboard')), 40 | ('sacd', 'SACD'), 41 | ('dat', 'DAT'), 42 | ('cassette', _('Cassette')), 43 | ('web', 'WEB'), 44 | ('bluray', 'Blu-Ray'), 45 | ) 46 | 47 | RELEASE_TYPES = ( 48 | ('album', _('Album')), 49 | ('soundtrack', _('Soundtrack')), 50 | ('ep', _('EP')), 51 | ('anthology', _('Anthology')), 52 | ('compilation', _('Compilation')), 53 | ('djmix', _('DJ Mix')), 54 | ('single', _('Single')), 55 | ('livealbum', _('Live Album')), 56 | ('remix', _('Remix')), 57 | ('bootleg', _('Bootleg')), 58 | ('interview', _('Interview')), 59 | ('mixtape', _('Mixtape')), 60 | ('unknown', _('Unknown')) 61 | ) 62 | -------------------------------------------------------------------------------- /batter/batter/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for batter project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | from os.path import abspath, dirname 18 | from sys import path 19 | 20 | SITE_ROOT = dirname(dirname(abspath(__file__))) 21 | path.append(SITE_ROOT) 22 | 23 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 24 | # if running multiple sites in the same mod_wsgi process. To fix this, use 25 | # mod_wsgi daemon mode with each site in its own daemon process, or use 26 | # os.environ["DJANGO_SETTINGS_MODULE"] = "jajaja.settings" 27 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "batter.settings.production") 28 | 29 | # This application object is used by any WSGI server configured to use this 30 | # file. This includes Django's development server, if the WSGI_APPLICATION 31 | # setting points here. 32 | from django.core.wsgi import get_wsgi_application 33 | application = get_wsgi_application() 34 | 35 | # Apply WSGI middleware here. 36 | # from helloworld.wsgi import HelloWorldApplication 37 | # application = HelloWorldApplication(application) 38 | -------------------------------------------------------------------------------- /batter/notifications/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | from django.conf import settings 4 | from django.utils.timezone import now 5 | 6 | 7 | class NotificationQuerySet(QuerySet): 8 | def mark_seen(self): 9 | return self.update(seen_at=now()) 10 | 11 | def unseen(self): 12 | return self.filter(seen_at=None) 13 | 14 | 15 | class NotificationManager(models.Manager): 16 | def get_queryset(self): 17 | return NotificationQuerySet(self.model) 18 | 19 | def by_user(self, user): 20 | return self.get_queryset().filter(recipient=user) 21 | 22 | 23 | class Notification(models.Model): 24 | recipient = models.ForeignKey( 25 | settings.AUTH_USER_MODEL, 26 | related_name='notifications' 27 | ) 28 | 29 | title = models.TextField(blank=False, null=False) 30 | body = models.TextField(blank=False, null=False) 31 | title_text = models.TextField(blank=True, null=False) 32 | body_text = models.TextField(blank=True, null=False) 33 | 34 | sent_at = models.DateTimeField(auto_now_add=True) 35 | seen_at = models.DateTimeField(null=True) 36 | 37 | objects = NotificationManager() 38 | 39 | def mark_seen(self): 40 | """ Mark a Notification as having been seen """ 41 | self.seen_at = now() 42 | return self 43 | 44 | def as_dict(self): 45 | """ Prepare a Notification for display, via e.g. JSON """ 46 | return { 47 | "text": { 48 | "title": self.title_text, 49 | "body": self.body_text, 50 | }, 51 | "html": { 52 | "title": self.title, 53 | "body": self.body, 54 | }, 55 | "seen": self.seen, 56 | "sent_at": self.sent_at, 57 | } 58 | 59 | @property 60 | def seen(self): 61 | return self.seen_at is not None 62 | 63 | class Meta: 64 | ordering = ['-sent_at'] 65 | -------------------------------------------------------------------------------- /batter/music/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from batter.test import LoggedInTestCase 6 | from ..models import Artist, Master, Release 7 | 8 | 9 | class ArtistDetailTests(LoggedInTestCase): 10 | def setUp(self): 11 | self.artist = Artist(name="Okkervil River", slug="Okkervil-River") 12 | self.artist.save() 13 | self.url = reverse("music_artist_detail", 14 | kwargs={'pk': self.artist.pk, 15 | 'slug': self.artist.slug}) 16 | super(ArtistDetailTests, self).setUp() 17 | 18 | def test_get(self): 19 | response = self.client.get(self.url) 20 | self.assertEquals(response.status_code, 200) 21 | 22 | def test_slug_redirect(self): 23 | response = self.client.get(reverse("music_artist_detail", 24 | kwargs={'pk': self.artist.pk, 25 | 'slug': 'wrong-slug'})) 26 | self.assertEquals(response.status_code, 301) 27 | 28 | 29 | class MasterDetailTests(LoggedInTestCase): 30 | def setUp(self): 31 | self.artist = Artist(name="Okkervil River", 32 | slug="Okkervil-River") 33 | self.artist.save() 34 | self.master = Master(name="Black Sheep Boy", 35 | slug="Black-Sheep-Boy") 36 | self.master.save() 37 | self.release = Release(name="Original", 38 | master=self.master) 39 | self.release.save() 40 | self.master.artists.add(self.artist) 41 | self.master.main = self.release 42 | self.master.save() 43 | self.url = reverse("music_master_detail", 44 | kwargs={'pk': self.master.pk, 45 | 'slug': self.master.slug}) 46 | super(MasterDetailTests, self).setUp() 47 | 48 | def test_get(self): 49 | response = self.client.get(self.url) 50 | self.assertEquals(response.status_code, 200) 51 | -------------------------------------------------------------------------------- /batter/templates/base.haml: -------------------------------------------------------------------------------- 1 | - load staticfiles 2 | 3 | !!! 5 4 | %html{lang: '{{ LANGUAGE_CODE }}'} 5 | %head 6 | %meta{charset: 'utf-8'} 7 | %title 8 | - block title-base 9 | - block title 10 | - if SITE_NAME 11 | \- {{ SITE_NAME }} 12 | %meta{name: 'viewport', content: 'width=device-width, initial-scale=1.0'} 13 | %meta{name: 'description', content: ''} 14 | %meta{name: 'author', content: ''} 15 | 16 | - block css-base 17 | %link{href: '{% static "css/bootstrap.min.css" %}', rel: 'stylesheet', type: 'text/css'} 18 | %link{href: '{% static "css/bootstrap-responsive.min.css" %}', rel: 'stylesheet', type: 'text/css'} 19 | %link{href: '{% static "css/project.css" %}', rel: 'stylesheet', type: 'text/css'} 20 | - block css 21 | 22 | /[if lt IE9] 23 | %script{src: 'http://html5shim.googlecode.com/svn/trunk/html5.js'} 24 | 25 | %body 26 | - block navbar-base 27 | .navbar.navbar-inverse.navbar-static-top 28 | .navbar-inner 29 | .container 30 | - block navbar 31 | %button.btn.btn-navbar{type: 'button', data-toggle:'collapse', data-target:'.nav-collapse'} 32 | %span.icon-bar 33 | %span.icon-bar 34 | %span.icon-bar 35 | - block brand-base 36 | %a.brand{href: '/'} 37 | - block brand 38 | Batter 39 | .nav-collapse.collapse 40 | -block nav-base 41 | %ul.nav 42 | - block nav 43 | %li 44 | %a{href: '{% url "home" %}'} Home 45 | - block header-base 46 | %header 47 | .container 48 | - block header 49 | .row 50 | .span12 51 | - block content-base 52 | #content 53 | .container 54 | - block content 55 | - block footer-base 56 | %footer 57 | .container 58 | - block footer 59 | .row 60 | .span 61 | - block js-base 62 | %script{src: '{% static "js/jquery.min.js" %}'} 63 | %script{src: '{% static "js/bootstrap.min.js" %}'} 64 | %script{src: '{% static "js/project.js" %}'} 65 | - block js 66 | -------------------------------------------------------------------------------- /batter/music/views/upload.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | from django.contrib.formtools.wizard.views import CookieWizardView 7 | from django.core.files.storage import FileSystemStorage 8 | from django.http import HttpResponse 9 | from django.shortcuts import render, redirect, get_object_or_404 10 | from django.template.defaultfilters import slugify 11 | 12 | from ..forms import TorrentTypeForm, ReleaseInfoForm, FileInfoForm 13 | 14 | 15 | def torrent_is_type(torrent_type): 16 | def check(wizard): 17 | cleaned_data = (wizard.get_cleaned_data_for_step('torrent_type') 18 | or {'type': 'none'}) 19 | return cleaned_data['type'] == torrent_type 20 | return check 21 | 22 | FORMS = [ 23 | ("torrent_type", TorrentTypeForm), 24 | ("release", ReleaseInfoForm), 25 | ("file", FileInfoForm) 26 | ] 27 | 28 | TEMPLATES = { 29 | "default": "music/upload/base.html", 30 | "release": "music/upload/release.html", 31 | } 32 | 33 | CONDITIONS = { 34 | "release": torrent_is_type('music'), 35 | "file": torrent_is_type('music') 36 | } 37 | 38 | 39 | class MusicUploadWizard(CookieWizardView): 40 | # TODO: use form_list once support for this gets released 41 | # (currently in django dev version) 42 | # form_list = [MusicUploadForm] 43 | file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 44 | 'tmp')) 45 | 46 | def get_template_names(self): 47 | try: 48 | return [TEMPLATES[self.steps.current]] 49 | except: 50 | return [TEMPLATES["default"]] 51 | 52 | def get_context_data(self, form, **kwargs): 53 | context = super(MusicUploadWizard, self).get_context_data(form=form, 54 | **kwargs) 55 | cleaned_data = (self.get_cleaned_data_for_step("torrent_type") 56 | or {'torrent_file': None}) 57 | if cleaned_data["torrent_file"]: 58 | context.update({'torrent_name': cleaned_data["torrent_file"].name}) 59 | return context 60 | 61 | def done(self, form_list, **kwargs): 62 | return HttpResponse('done') 63 | -------------------------------------------------------------------------------- /batter/batter/settings/production.py: -------------------------------------------------------------------------------- 1 | """Production settings and globals.""" 2 | 3 | 4 | from os import environ 5 | 6 | from base import * 7 | 8 | # Normally you should not import ANYTHING from Django directly 9 | # into your settings, but ImproperlyConfigured is an exception. 10 | from django.core.exceptions import ImproperlyConfigured 11 | 12 | 13 | def get_env_setting(setting): 14 | """ Get the environment setting or return exception """ 15 | try: 16 | return environ[setting] 17 | except KeyError: 18 | error_msg = "Set the %s env variable" % setting 19 | raise ImproperlyConfigured(error_msg) 20 | 21 | INSTALLED_APPS += ('gunicorn',) 22 | 23 | ########## EMAIL CONFIGURATION 24 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 25 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 26 | 27 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host 28 | EMAIL_HOST = environ.get('EMAIL_HOST', 'smtp.gmail.com') 29 | 30 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host-password 31 | EMAIL_HOST_PASSWORD = environ.get('EMAIL_HOST_PASSWORD', '') 32 | 33 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user 34 | EMAIL_HOST_USER = environ.get('EMAIL_HOST_USER', 'your_email@example.com') 35 | 36 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-port 37 | EMAIL_PORT = environ.get('EMAIL_PORT', 587) 38 | 39 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 40 | EMAIL_SUBJECT_PREFIX = '[%s] ' % SITE_NAME 41 | 42 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-use-tls 43 | EMAIL_USE_TLS = True 44 | 45 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email 46 | SERVER_EMAIL = EMAIL_HOST_USER 47 | ########## END EMAIL CONFIGURATION 48 | 49 | 50 | ########## DATABASE CONFIGURATION 51 | DATABASES = {} 52 | ########## END DATABASE CONFIGURATION 53 | 54 | 55 | ########## CACHE CONFIGURATION 56 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#caches 57 | CACHES = {} 58 | ########## END CACHE CONFIGURATION 59 | 60 | 61 | ########## SECRET CONFIGURATION 62 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 63 | SECRET_KEY = get_env_setting('SECRET_KEY') 64 | ########## END SECRET CONFIGURATION 65 | -------------------------------------------------------------------------------- /batter/batter/settings/local.py: -------------------------------------------------------------------------------- 1 | """Development settings and globals.""" 2 | 3 | 4 | from os.path import join, normpath 5 | 6 | from base import * 7 | 8 | ########## DEBUG CONFIGURATION 9 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug 10 | DEBUG = True 11 | 12 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug 13 | TEMPLATE_DEBUG = DEBUG 14 | ########## END DEBUG CONFIGURATION 15 | 16 | 17 | ########## EMAIL CONFIGURATION 18 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 19 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 20 | ########## END EMAIL CONFIGURATION 21 | 22 | 23 | ########## DATABASE CONFIGURATION 24 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': normpath(join(DJANGO_ROOT, 'dev.db')), 29 | 'USER': '', 30 | 'PASSWORD': '', 31 | 'HOST': '', 32 | 'PORT': '', 33 | } 34 | } 35 | ########## END DATABASE CONFIGURATION 36 | 37 | 38 | ########## CACHE CONFIGURATION 39 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#caches 40 | CACHES = { 41 | 'default': { 42 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 43 | } 44 | } 45 | ########## END CACHE CONFIGURATION 46 | 47 | 48 | ########## TOOLBAR CONFIGURATION 49 | # See: https://github.com/django-debug-toolbar/django-debug-toolbar 50 | # #installation 51 | INSTALLED_APPS += ( 52 | 'debug_toolbar', 53 | ) 54 | 55 | # See: https://github.com/django-debug-toolbar/django-debug-toolbar 56 | # #installation 57 | INTERNAL_IPS = ('127.0.0.1',) 58 | 59 | # See: https://github.com/django-debug-toolbar/django-debug-toolbar 60 | # #installation 61 | MIDDLEWARE_CLASSES += ( 62 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 63 | ) 64 | 65 | DEBUG_TOOLBAR_CONFIG = { 66 | 'INTERCEPT_REDIRECTS': False 67 | } 68 | ########## END TOOLBAR CONFIGURATION 69 | 70 | ## Tracker CONFIGURATION 71 | TRACKER_ANNOUNCE = 'http://localhost:7070/announce/' 72 | ## 73 | 74 | ########## HAYSTACK SEARCH CONFIGURATION 75 | HAYSTACK_CONNECTIONS = { 76 | 'default': { 77 | 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', # noqa 78 | 'URL': 'http://127.0.0.1:9200/', 79 | 'INDEX_NAME': 'haystack', 80 | }, 81 | } 82 | ########## END HAYSTACK SEARCH CONFIGURATION 83 | -------------------------------------------------------------------------------- /vagrant/puppet/manifests/default.pp: -------------------------------------------------------------------------------- 1 | #set up defaults 2 | 3 | Exec { path => '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/' } 4 | exec { 'echo this works': } 5 | 6 | #set up updated apt-get repos 7 | 8 | group { 'puppet': ensure => 'present' } 9 | 10 | exec { 'apt-get update': 11 | command => '/usr/bin/apt-get update' 12 | } 13 | 14 | 15 | #install system packages that some python libs depend on 16 | 17 | package { 'python-dev': 18 | ensure => present, 19 | require => Exec['apt-get update'] 20 | } 21 | 22 | package { 'python-virtualenv': 23 | ensure => present, 24 | require => Exec['apt-get update'] 25 | } 26 | 27 | package { 'redis-server': 28 | ensure => present, 29 | require => Exec['apt-get update'] 30 | } 31 | 32 | package { 'libtag1-dev': 33 | ensure => present, 34 | require => Exec['apt-get update'] 35 | } 36 | 37 | package { 'git': 38 | ensure => present, 39 | require => Exec['apt-get update'] 40 | } 41 | 42 | package { 'zlib1g-dev': 43 | ensure => present, 44 | require => Exec['apt-get update'] 45 | } 46 | 47 | package { 'libxml2-dev': 48 | ensure => present, 49 | require => Exec['apt-get update'] 50 | } 51 | 52 | package { 'libxslt-dev': 53 | ensure => present, 54 | require => Exec['apt-get update'] 55 | } 56 | 57 | package { 'vim': 58 | ensure => present, 59 | require => Exec['apt-get update'] 60 | } 61 | 62 | package { 'virtualenvwrapper': 63 | ensure => latest, 64 | provider => pip, 65 | } 66 | 67 | package { 'tmux': 68 | ensure => present, 69 | require => Exec['apt-get update'] 70 | } 71 | 72 | service { 'redis-server': 73 | ensure => running, 74 | require => Package['redis-server'] 75 | } 76 | 77 | 78 | # add/setup virtualenvwrapper to auto start 79 | 80 | file { '.bash_aliases': 81 | path => '/home/vagrant/.bash_aliases', 82 | source => '/vagrant/files/bash_aliases', 83 | } 84 | 85 | # add a tmux config that acts more like screen 86 | 87 | file { '.tmux.conf': 88 | path => '/home/vagrant/.tmux.conf', 89 | source => '/vagrant/files/tmux.conf', 90 | require => Package['tmux'] 91 | } 92 | 93 | file { '/vagrant/files/install_venv.sh': 94 | ensure => 'present', 95 | mode => '0777', 96 | source => '/vagrant/files/install_venv.sh', 97 | } 98 | 99 | exec { '/vagrant/files/install_venv.sh': 100 | require => [ 101 | Package['python-virtualenv'], 102 | Package['virtualenvwrapper'], 103 | File['.bash_aliases'], 104 | File['/vagrant/files/install_venv.sh'], 105 | ], 106 | logoutput => 'on_failure' 107 | } 108 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Batter is released under a BSD 2-Clause license, reproduced below. 2 | 3 | Copyright (c) 2013, Edgewyn 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 20 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | Batter is derived in part from twoscoops/django-twoscoops-project, which 30 | is released under the following license: 31 | 32 | > Copyright (c) 2013 Audrey Roy, Daniel Greenfeld, and contributors. 33 | > 34 | > Permission is hereby granted, free of charge, to any person 35 | > obtaining a copy of this software and associated documentation 36 | > files (the "Software"), to deal in the Software without 37 | > restriction, including without limitation the rights to use, 38 | > copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | > copies of the Software, and to permit persons to whom the 40 | > Software is furnished to do so, subject to the following 41 | > conditions: 42 | > 43 | > The above copyright notice and this permission notice shall be 44 | > included in all copies or substantial portions of the Software. 45 | > 46 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 47 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 48 | > OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 49 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 50 | > HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 51 | > WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 52 | > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 53 | > OTHER DEALINGS IN THE SOFTWARE. 54 | -------------------------------------------------------------------------------- /batter/notifications/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | 4 | from ..models import Notification 5 | 6 | 7 | class NotificationTests(TestCase): 8 | def setUp(self): 9 | self.samantha = User.objects.create_user( 10 | 'samantha', 11 | 'samantha@example.com', 12 | 'soliloquy' 13 | ) 14 | self.joe = User.objects.create_user( 15 | 'joe', 16 | 'joe@example.com', 17 | 'antiphony' 18 | ) 19 | self.samantha_mail, _ = Notification.objects.get_or_create( 20 | recipient=self.samantha, 21 | title='You\'ve got mail!', 22 | body='joe sent you a message', 23 | title_text='You\'ve got mail!', 24 | body_text='joe sent you a message' 25 | ) 26 | 27 | def test_model_mark_seen(self): 28 | self.assertEquals(self.samantha_mail.seen, False) 29 | self.assertIsNone(self.samantha_mail.seen_at) 30 | 31 | self.samantha_mail.mark_seen().save() 32 | 33 | self.assertEquals(self.samantha_mail.seen, True) 34 | self.assertIsNotNone(self.samantha_mail.seen_at) 35 | 36 | def test_manager_by_user(self): 37 | results = Notification.objects.by_user(self.samantha) 38 | 39 | self.assertIn(self.samantha_mail, results) 40 | self.assertEqual(len(results), 1) 41 | 42 | def test_manager_by_other_user(self): 43 | results = Notification.objects.by_user(self.joe) 44 | 45 | self.assertEqual(len(results), 0) 46 | 47 | def test_queryset_unseen(self): 48 | results = Notification.objects.by_user(self.samantha).unseen() 49 | 50 | self.assertIn(self.samantha_mail, results) 51 | 52 | self.samantha_mail.mark_seen().save() 53 | 54 | results = Notification.objects.by_user(self.samantha).unseen() 55 | 56 | self.assertNotIn(self.samantha_mail, results) 57 | 58 | def test_queryset_mark_seen(self): 59 | self.assertEquals(self.samantha_mail.seen, False) 60 | 61 | results = Notification.objects.by_user(self.samantha).unseen() 62 | results.mark_seen() 63 | 64 | self.samantha_mail = Notification.objects.get( 65 | pk=self.samantha_mail.pk 66 | ) 67 | 68 | self.assertEquals(self.samantha_mail.seen, True) 69 | 70 | def test_model_as_dict(self): 71 | obj = self.samantha_mail.as_dict() 72 | self.assertIn("text", obj) 73 | self.assertIn("html", obj) 74 | self.assertIn("seen", obj) 75 | self.assertEquals(obj['seen'], False) 76 | self.assertIsNotNone(obj['sent_at']) 77 | self.assertEquals(obj['text']['title'], "You've got mail!") 78 | self.assertEquals(obj['text']['body'], "joe sent you a message") 79 | self.assertEquals(obj['html']['title'], "You've got mail!") 80 | self.assertEquals(obj['html']['body'], "joe sent you a message") 81 | -------------------------------------------------------------------------------- /batter/torrents/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from batter.test import LoggedInTestCase 6 | 7 | from .local_settings import TEST_FILE_PATH 8 | from ..models import Torrent 9 | 10 | 11 | class UploadTorrentTests(LoggedInTestCase): 12 | def setUp(self): 13 | self.url = reverse("torrents_torrent_upload") 14 | super(UploadTorrentTests, self).setUp() 15 | 16 | def test_get(self): 17 | response = self.client.get(self.url) 18 | self.assertEquals(response.status_code, 200) 19 | 20 | def test_post_valid_torrent(self): 21 | with open(TEST_FILE_PATH, 'rb') as fp: 22 | response = self.client.post(self.url, {'torrent_file': fp}) 23 | 24 | self.assertEquals(response.status_code, 302) 25 | self.assertEquals(Torrent.objects.count(), 1) 26 | 27 | def test_post_duplicate_torrent(self): 28 | with open(TEST_FILE_PATH, 'rb') as fp: 29 | self.client.post(self.url, {'torrent_file': fp}) 30 | fp.seek(0) 31 | response = self.client.post(self.url, {'torrent_file': fp}) 32 | 33 | self.assertEquals(response.status_code, 409) 34 | self.assertEquals(Torrent.objects.count(), 1) 35 | 36 | def test_logged_out(self): 37 | self.client.logout() 38 | response = self.client.get(self.url) 39 | self.assertEquals(response.status_code, 302) 40 | 41 | 42 | class ViewTorrentTests(LoggedInTestCase): 43 | def setUp(self): 44 | with open(TEST_FILE_PATH, 'rb') as test_file: 45 | self.torrent = Torrent.from_torrent_file(test_file) 46 | 47 | self.torrent.save() 48 | self.torrent_url = reverse("torrents_torrent_view", kwargs={ 49 | 'pk': self.torrent.pk 50 | }) 51 | super(ViewTorrentTests, self).setUp() 52 | 53 | def test_existing_torrent(self): 54 | response = self.client.get(self.torrent_url) 55 | self.assertEquals(response.status_code, 200) 56 | 57 | def test_nonexisting_torrent(self): 58 | response = self.client.get(reverse("torrents_torrent_view", kwargs={ 59 | 'pk': 42 60 | })) 61 | self.assertEquals(response.status_code, 404) 62 | 63 | 64 | class DownloadTorrentTests(LoggedInTestCase): 65 | def setUp(self): 66 | with open(TEST_FILE_PATH, 'rb') as test_file: 67 | self.torrent = Torrent.from_torrent_file(test_file) 68 | self.torrent_size = test_file.tell() 69 | test_file.seek(0) 70 | self.raw_torrent = test_file.read() 71 | self.torrent.save() 72 | self.torrent_url = reverse("torrents_torrent_download", 73 | kwargs={'pk': self.torrent.pk}) 74 | super(DownloadTorrentTests, self).setUp() 75 | 76 | def test_existing_torrent(self): 77 | response = self.client.get(self.torrent_url) 78 | self.assertEquals( 79 | int(response['Content-Length']), 80 | int(self.torrent_size) 81 | ) 82 | self.assertEquals( 83 | response['Content-Disposition'], 84 | 'attachment; filename=archlinux-20130401-dualiso.torrent' 85 | ) 86 | self.assertEquals(response.content, self.raw_torrent) 87 | 88 | def test_nonexisting_torrent(self): 89 | response = self.client.get(reverse("torrents_torrent_download", 90 | kwargs={'pk': 42})) 91 | self.assertEquals(response.status_code, 404) 92 | -------------------------------------------------------------------------------- /batter/torrents/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import hashlib 4 | 5 | from django.test import TestCase 6 | 7 | from .local_settings import TEST_FILE_PATH 8 | from ..models import Torrent 9 | 10 | 11 | sha1 = lambda data: hashlib.sha1(data).hexdigest() 12 | 13 | 14 | class TorrentTests(TestCase): 15 | def test_from_torrent_file(self): 16 | with open(TEST_FILE_PATH, 'rb') as test_file: 17 | torrent = Torrent.from_torrent_file(test_file) 18 | test_file.seek(0) 19 | orig_torrent_str = test_file.read() 20 | 21 | self.assertEquals(torrent.name, "archlinux-2013.04.01-dual.iso") 22 | self.assertEquals(torrent.as_bencoded_string(), orig_torrent_str) 23 | # note that the torrent file is slightly modified 24 | # I've removed the url-list dictionary element 25 | # since we have no support for that 26 | 27 | def test_to_torrent_singlefile(self): 28 | torrent = Torrent() 29 | torrent.name = "my.little.pwnie.zip" 30 | torrent.announce = "http://example.com/announce" 31 | torrent.piece_length = 32768 32 | torrent.pieces = "09bc090d67579eaed539c883b956d265a7975096" 33 | torrent.is_private = True 34 | torrent.length = 32768 35 | torrent_str = torrent.as_bencoded_string() # this shouldn't throw 36 | self.assertEquals( 37 | sha1(torrent_str), b"4d9e46d46fcbd23d89c7e1366646a1ca7052a2bb") 38 | 39 | def test_to_torrent_singlefile_with_md5sum(self): 40 | torrent = Torrent() 41 | torrent.name = "my.little.pwnie.zip" 42 | torrent.announce = "http://example.com/announce" 43 | torrent.piece_length = 32768 44 | torrent.pieces = "09bc090d67579eaed539c883b956d265a7975096" 45 | torrent.is_private = True 46 | torrent.length = 32768 47 | torrent.md5sum = "0b784b963828308665f509173676bbcd" 48 | torrent_str = torrent.as_bencoded_string() # this shouldn't throw 49 | self.assertEquals( 50 | sha1(torrent_str), b"fe1fcf4a3c635445d6f998b0fdfab652465099f0") 51 | 52 | def test_to_torrent_multifile(self): 53 | torrent = Torrent() 54 | torrent.name = "my.little.pwnies" 55 | torrent.announce = "http://example.com/announce" 56 | torrent.announce_list = [ 57 | u'http://example.com/announce', 58 | u'http://backup1.example.com/announce' 59 | ] 60 | torrent.piece_length = 32768 61 | torrent.pieces = b"09bc090d67579eaed539c883b956d265a7975096" 62 | torrent.is_private = False 63 | torrent.length = None 64 | torrent.encoding = 'hex' 65 | torrent.files = [ 66 | { 67 | 'length': 235, 68 | 'md5sum': b"0b784b963828308665f509173676bbcd", 69 | 'path': ['dir1', 'dir2', 'file.ext'], 70 | }, 71 | { 72 | 'length': 435, 73 | 'md5sum': b"784b0b963828308665f509173676bbcd", 74 | 'path': ['moop.dir'], 75 | } 76 | ] 77 | torrent_str = torrent.as_bencoded_string() # this shouldn't throw 78 | self.assertEquals( 79 | sha1(torrent_str), b"41c49ebb8d4aa7a977b9642da9512331a9abfe10") 80 | 81 | def test_torrent_unicode(self): 82 | torrent = Torrent() 83 | torrent.name = "hi" 84 | self.assertEquals(unicode(torrent), torrent.name) 85 | 86 | def test_absolute_url(self): 87 | # just poke it 88 | torrent = Torrent() 89 | torrent.id = 9 90 | torrent.get_absolute_url() 91 | -------------------------------------------------------------------------------- /batter/notifications/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.test import TestCase 5 | from django.contrib.auth.models import User 6 | 7 | from ..models import Notification 8 | 9 | 10 | class BaseNotificationTests(TestCase): 11 | def setUp(self): 12 | self.samantha = User.objects.create_user( 13 | 'samantha', 14 | 'samantha@example.com', 15 | 'soliloquy' 16 | ) 17 | self.samantha_mail, _ = Notification.objects.get_or_create( 18 | recipient=self.samantha, 19 | title='You\'ve got mail!', 20 | body='joe sent you a message', 21 | title_text='You\'ve got mail!', 22 | body_text='joe sent you a message' 23 | ) 24 | 25 | def generate_bunk(self, num): 26 | bunk = [] 27 | for i in range(num): 28 | n, _ = Notification.objects.get_or_create( 29 | recipient=self.samantha, 30 | title='You\'ve got mail! ' + str(i), 31 | body='joe sent you a message', 32 | title_text='You\'ve got mail!' + str(i), 33 | body_text='joe sent you a message' 34 | ) 35 | bunk.append(n) 36 | return bunk 37 | 38 | def login(self): 39 | self.client.login(username='samantha', password='soliloquy') 40 | 41 | 42 | class NotificationAPITests(BaseNotificationTests): 43 | def fetch_list_response(self): 44 | url = reverse('notifications_list') 45 | response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 46 | return response 47 | 48 | def fetch_list(self): 49 | self.login() 50 | response = self.fetch_list_response() 51 | self.assertEquals(response.status_code, 200) 52 | data = json.loads(response.content) 53 | return data 54 | 55 | def test_list_authentication(self): 56 | response = self.fetch_list_response() 57 | self.assertEquals(response.status_code, 302) 58 | self.assertIn('signin', response['Location']) 59 | 60 | def test_list_pagination(self): 61 | self.generate_bunk(60) 62 | data = self.fetch_list() 63 | self.assertIn('total', data) 64 | self.assertEquals(data['total'], 61) 65 | self.assertIn('pages', data) 66 | self.assertIn('count', data['pages']) 67 | self.assertEquals(len(data['results']), 10) 68 | self.assertEquals(data['pages']['count'], 7) 69 | 70 | def test_list_single(self): 71 | data = self.fetch_list() 72 | self.assertEquals(len(data['results']), 1) 73 | result = data['results'][0] 74 | self.assertEquals(result['seen'], False) 75 | 76 | 77 | class NotificationHTMLTests(BaseNotificationTests): 78 | def fetch_list_response(self, data={}): 79 | url = reverse('notifications_list') 80 | response = self.client.get(url, data=data) 81 | return response 82 | 83 | def fetch_list(self, data={}): 84 | self.login() 85 | response = self.fetch_list_response(data) 86 | self.assertEquals(response.status_code, 200) 87 | return response 88 | 89 | def test_list_authentication(self): 90 | response = self.fetch_list_response() 91 | self.assertEquals(response.status_code, 302) 92 | self.assertIn('signin', response['Location']) 93 | 94 | def test_list_pagination(self): 95 | self.generate_bunk(60) 96 | response = self.fetch_list() 97 | self.assertEquals(len(response.context['object_list']), 20) 98 | self.assertIsNotNone(response.context['paginator']) 99 | self.assertEquals(response.context['is_paginated'], True) 100 | 101 | def test_list_pagination_page_2(self): 102 | self.generate_bunk(60) 103 | response = self.fetch_list(data={'page': 2}) 104 | self.assertEquals(len(response.context['object_list']), 20) 105 | -------------------------------------------------------------------------------- /batter/notifications/migrations/0002_auto__add_field_notification_seen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Notification.seen' 12 | db.add_column(u'notifications_notification', 'seen', 13 | self.gf('django.db.models.fields.BooleanField')(default=False), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'Notification.seen' 19 | db.delete_column(u'notifications_notification', 'seen') 20 | 21 | 22 | models = { 23 | u'auth.group': { 24 | 'Meta': {'object_name': 'Group'}, 25 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 27 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 28 | }, 29 | u'auth.permission': { 30 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 31 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 33 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 35 | }, 36 | u'auth.user': { 37 | 'Meta': {'object_name': 'User'}, 38 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 39 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 40 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 41 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 42 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 44 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 46 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 47 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 48 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 49 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 50 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 51 | }, 52 | u'contenttypes.contenttype': { 53 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 54 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 55 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 56 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 57 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 58 | }, 59 | u'notifications.notification': { 60 | 'Meta': {'object_name': 'Notification'}, 61 | 'body': ('django.db.models.fields.CharField', [], {'max_length': '512'}), 62 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notifications'", 'to': u"orm['auth.User']"}), 64 | 'seen': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 65 | 'seen_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), 66 | 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 67 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 68 | } 69 | } 70 | 71 | complete_apps = ['notifications'] -------------------------------------------------------------------------------- /batter/notifications/migrations/0004_auto__del_field_notification_seen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Deleting field 'Notification.seen' 12 | db.delete_column(u'notifications_notification', 'seen') 13 | 14 | 15 | def backwards(self, orm): 16 | # Adding field 'Notification.seen' 17 | db.add_column(u'notifications_notification', 'seen', 18 | self.gf('django.db.models.fields.BooleanField')(default=False), 19 | keep_default=False) 20 | 21 | 22 | models = { 23 | u'auth.group': { 24 | 'Meta': {'object_name': 'Group'}, 25 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 27 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 28 | }, 29 | u'auth.permission': { 30 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 31 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 33 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 35 | }, 36 | u'auth.user': { 37 | 'Meta': {'object_name': 'User'}, 38 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 39 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 40 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 41 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 42 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 44 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 46 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 47 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 48 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 49 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 50 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 51 | }, 52 | u'contenttypes.contenttype': { 53 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 54 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 55 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 56 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 57 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 58 | }, 59 | u'notifications.notification': { 60 | 'Meta': {'ordering': "['-sent_at']", 'object_name': 'Notification'}, 61 | 'body': ('django.db.models.fields.TextField', [], {}), 62 | 'body_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 63 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 64 | 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notifications'", 'to': u"orm['auth.User']"}), 65 | 'seen_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), 66 | 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 67 | 'title': ('django.db.models.fields.TextField', [], {}), 68 | 'title_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) 69 | } 70 | } 71 | 72 | complete_apps = ['notifications'] -------------------------------------------------------------------------------- /batter/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'UserProfile' 12 | db.create_table(u'core_userprofile', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('mugshot', self.gf('django.db.models.fields.files.ImageField')(max_length=100, blank=True)), 15 | ('privacy', self.gf('django.db.models.fields.CharField')(default='registered', max_length=15)), 16 | ('language', self.gf('django.db.models.fields.CharField')(default='en', max_length=5)), 17 | ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='user_profile', unique=True, to=orm['auth.User'])), 18 | )) 19 | db.send_create_signal(u'core', ['UserProfile']) 20 | 21 | 22 | def backwards(self, orm): 23 | # Deleting model 'UserProfile' 24 | db.delete_table(u'core_userprofile') 25 | 26 | 27 | models = { 28 | u'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | u'auth.permission': { 35 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 38 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | u'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 47 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 56 | }, 57 | u'contenttypes.contenttype': { 58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 63 | }, 64 | u'core.userprofile': { 65 | 'Meta': {'object_name': 'UserProfile'}, 66 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '5'}), 68 | 'mugshot': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}), 69 | 'privacy': ('django.db.models.fields.CharField', [], {'default': "'registered'", 'max_length': '15'}), 70 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'user_profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) 71 | } 72 | } 73 | 74 | complete_apps = ['core'] -------------------------------------------------------------------------------- /batter/music/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.db import models 5 | from django.utils.encoding import python_2_unicode_compatible 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from model_utils import Choices 9 | from model_utils.models import TimeStampedModel 10 | 11 | optional = {'blank': True, 'null': True} 12 | 13 | 14 | @python_2_unicode_compatible 15 | class MusicBaseModel(TimeStampedModel): 16 | name = models.TextField() 17 | slug = models.SlugField(max_length=255) 18 | image = models.ImageField(upload_to="music_image", **optional) 19 | 20 | class Meta: 21 | abstract = True 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | 27 | @python_2_unicode_compatible 28 | class MusicUpload(TimeStampedModel): 29 | release = models.ForeignKey('Release') 30 | torrent = models.OneToOneField('torrents.Torrent') 31 | format = Choices(('mp3', _('MP3')), 32 | ('flac', _('FLAC')), 33 | ('aac', _('AAC')), 34 | ('ac3', _('AC3')), 35 | ('dts', _('DTS'))) 36 | bitrate = Choices(('192', _('192')), 37 | ('apsvbr', _('APS (VBR)')), 38 | ('v2vbr', _('V2 (VBR)')), 39 | ('v1vbr', _('V1 (VBR)')), 40 | ('256', _('256')), 41 | ('apxvbr', _('APX (VBR)')), 42 | ('v0vbr', _('V0 (VBR)')), 43 | ('320', _('320')), 44 | ('lossless', _('Lossless')), 45 | ('24bitlossless', _('24bit Lossless')), 46 | ('v8vbr', _('V8 (VBR)')), 47 | ('other', _('Other'))) 48 | 49 | class Meta: 50 | verbose_name = _('Music Upload') 51 | verbose_name_plural = _('Music Uploads') 52 | 53 | def __str__(self): 54 | return "{} - {}".format(self.release, self.torrent) 55 | 56 | 57 | class Artist(MusicBaseModel): 58 | summary = models.TextField(blank=True) 59 | # TODO: Add more types of url (last.fm, spotify, etc)? 60 | url = models.URLField(blank=True) 61 | 62 | class Meta: 63 | verbose_name = _('Artist') 64 | verbose_name_plural = _('Artists') 65 | 66 | def get_absolute_url(self): 67 | return reverse('music_artist_detail', 68 | kwargs={'pk': self.pk, 'slug': self.slug}) 69 | 70 | 71 | @python_2_unicode_compatible 72 | class Master(MusicBaseModel): 73 | artists = models.ManyToManyField('Artist', **optional) 74 | main = models.ForeignKey('Release', related_name='+', **optional) 75 | 76 | class Meta: 77 | verbose_name = _('Master') 78 | verbose_name_plural = _('Masters') 79 | 80 | def get_absolute_url(self): 81 | return reverse('music_master_detail', 82 | kwargs={'pk': self.pk, 'slug': self.slug}) 83 | 84 | def __str__(self): 85 | return "{} - {}".format(", ".join(artist.name 86 | for artist 87 | in self.artists.all()), 88 | self.name) 89 | 90 | 91 | @python_2_unicode_compatible 92 | class Release(TimeStampedModel): 93 | master = models.ForeignKey('Master') 94 | label = models.ForeignKey('Label', **optional) 95 | release_type = Choices(('album', _('Album')), 96 | ('soundtrack', _('Soundtrack')), 97 | ('ep', _('EP')), 98 | ('anthology', _('Anthology')), 99 | ('compilation', _('Compilation')), 100 | ('djmix', _('DJ Mix')), 101 | ('single', _('Single')), 102 | ('livealbum', _('Live Album')), 103 | ('remix', _('Remix')), 104 | ('bootleg', _('Bootleg')), 105 | ('interview', _('Interview')), 106 | ('mixtape', _('Mixtape')), 107 | ('concertrecording', _('Concert Recording')), 108 | ('demo', _('Demo')), 109 | ('unknown', _('Unknown'))) 110 | year = models.PositiveIntegerField(**optional) 111 | catalog_num = models.TextField(blank=True) 112 | name = models.TextField(blank=True) 113 | scene = models.BooleanField() 114 | 115 | class Meta: 116 | verbose_name = _('Release') 117 | verbose_name_plural = _('Releases') 118 | 119 | def __str__(self): 120 | return "{} ({})".format(self.master, self.name) 121 | 122 | 123 | @python_2_unicode_compatible 124 | class Label(TimeStampedModel): 125 | name = models.TextField() 126 | parent_label = models.ForeignKey('self', **optional) 127 | 128 | class Meta: 129 | verbose_name = _('Label') 130 | verbose_name_plural = _('Labels') 131 | 132 | def __str__(self): 133 | return "{}".format(self.name) 134 | -------------------------------------------------------------------------------- /batter/notifications/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Notification' 12 | db.create_table(u'notifications_notification', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('recipient', self.gf('django.db.models.fields.related.ForeignKey')(related_name='notifications', to=orm['auth.User'])), 15 | ('title', self.gf('django.db.models.fields.CharField')(max_length=64)), 16 | ('body', self.gf('django.db.models.fields.CharField')(max_length=512)), 17 | ('sent_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 18 | ('seen_at', self.gf('django.db.models.fields.DateTimeField')(null=True)), 19 | )) 20 | db.send_create_signal(u'notifications', ['Notification']) 21 | 22 | 23 | def backwards(self, orm): 24 | # Deleting model 'Notification' 25 | db.delete_table(u'notifications_notification') 26 | 27 | 28 | models = { 29 | u'auth.group': { 30 | 'Meta': {'object_name': 'Group'}, 31 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 32 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 33 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 34 | }, 35 | u'auth.permission': { 36 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 37 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 38 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 39 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 40 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 41 | }, 42 | u'auth.user': { 43 | 'Meta': {'object_name': 'User'}, 44 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 45 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 46 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 48 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 49 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 50 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 52 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 53 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 54 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 55 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 56 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 57 | }, 58 | u'contenttypes.contenttype': { 59 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 60 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 61 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 64 | }, 65 | u'notifications.notification': { 66 | 'Meta': {'object_name': 'Notification'}, 67 | 'body': ('django.db.models.fields.CharField', [], {'max_length': '512'}), 68 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notifications'", 'to': u"orm['auth.User']"}), 70 | 'seen_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), 71 | 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 72 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 73 | } 74 | } 75 | 76 | complete_apps = ['notifications'] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Batter 2 | ====== 3 | 4 | It makes Waffles (and other tasty things)! 5 | 6 | [![Build Status](https://travis-ci.org/wafflesfm/batter.png?branch=develop)](https://travis-ci.org/wafflesfm/batter) [![Coverage Status](https://coveralls.io/repos/wafflesfm/batter/badge.png?branch=develop)](https://coveralls.io/r/wafflesfm/batter?branch=develop) 7 | 8 | To develop on this project follow these steps: 9 | 10 | 1. Download the code 11 | 2. Vagrant up! 12 | 3. Create the database schema 13 | 4. Run Batter 14 | 5. Contribute changes! 15 | 16 | Download the code 17 | ----------------- 18 | 19 | You can get the most recent copy of Batter by cloning this repository: 20 | 21 | git clone git://github.com/wafflesfm/batter.git 22 | 23 | which will copy all of Batter into a new folder called `batter`. 24 | 25 | Vagrant up! 26 | ----------- 27 | 28 | Vagrant is a way to create and configure lightweight, reproducible, and 29 | portable development environments. We use it to keep the Batter runtime 30 | in sync across our machines. [Download it](http://www.vagrantup.com/) 31 | and then `cd vagrant` followed by `vagrant up` to create your working 32 | development environment. Once your environment has been created, run 33 | `vagrant ssh` and follow the next two instructions. 34 | 35 | Create the Database Schema 36 | -------------------------- 37 | 38 | In your terminal, type 39 | 40 | (batter) $ python batter/batter/manage.py syncdb 41 | (batter) $ python batter/batter/manage.py migrate 42 | 43 | Run Batter 44 | ---------- 45 | 46 | In your terminal, type 47 | 48 | (batter) $ python batter/batter/manage.py runserver 0.0.0.0:8000 49 | 50 | You should now be able to open your browser to http://localhost:8080/ and 51 | use the site. 52 | 53 | Yes, you're running the server on port 8000 in your vagrant environment, 54 | but vagrant port-forwards environment:8000 to localhost:8080. 55 | 56 | Contribute changes! 57 | ------------------- 58 | 59 | So you want to contribute to Batter, you devilishly smart and attractive 60 | person? Awesome! 61 | 62 | First off, fork the [wafflesfm/batter](https://github.com/wafflesfm/batter) 63 | repository to your own github account. After you've cloned your own fork, 64 | add the wafflesfm repo as the `upstream` remote with 65 | 66 | $ git remote add upstream git@github.com:wafflesfm/batter 67 | 68 | (If you have commit access to wafflesfm/batter, you don't need to fork 69 | or add the upstream remote. The rest of this section still applies to you!) 70 | 71 | ### Styleguide 72 | 73 | Please follow these **coding standards** when writing code: 74 | 75 | * Poocoo [styleguide] [poocoo] for all Python code. 76 | * For Django-specific code follow internal Django [coding style] [django]. 77 | * Additionally, since we want Batter to be Python3 compatible, 78 | make sure your code complies with Django [guidelines] [python3] 79 | on Python3 compatibility. 80 | 81 | [poocoo]: http://www.pocoo.org/internal/styleguide/#styleguide 82 | [django]: http://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style 83 | [python3]: https://docs.djangoproject.com/en/dev/topics/python3 84 | 85 | ### i18n 86 | 87 | We want Batter to be **translatable**, so please use Django's builtin 88 | internationalization 89 | [helpers](https://docs.djangoproject.com/en/dev/topics/i18n/translation) 90 | for all strings displayed to the user. 91 | 92 | ### Workflow 93 | 94 | We use [git-flow](https://github.com/nvie/gitflow) for our git workflow. 95 | Debian/Ubuntu users can `sudo aptitude install git-flow`, and users of 96 | other operating systems can find installation instructions 97 | [here](https://github.com/nvie/gitflow/wiki/Installation). 98 | 99 | Once you have git-flow installed, you need to set it up for your batter 100 | repository. Setting up git-flow is a one-time thing. After you clone the repository 101 | and installed git-flow, navigate to your batter project folder and run 102 | 103 | $ git flow init 104 | 105 | Accept all the defaults. After the setup wizard is done, your "stable" 106 | branch should be **master**, "development" branch should be **develop**, 107 | "feature" prefix should be **feature**, "release" prefix should be 108 | **release**, "hotfix" prefix should be **hotfix**, and "support" prefix 109 | should be **support**. 110 | 111 | After this, you can use git flow to work on new features or fix existing 112 | ones. The following articles should help you understand how git-flow works. 113 | 114 | * http://nvie.com/posts/a-successful-git-branching-model/ - the original 115 | blog post introducing the git workflow 116 | 117 | * http://yakiloo.com/getting-started-git-flow/ - a practical introduction 118 | to using the git-flow plugin 119 | 120 | * http://qq.is/article/git-flow-on-github - using git-flow in tandem with 121 | GitHub pull requests. 122 | 123 | It is *strongly recommended* that 124 | even committers who have access to the repository use GitHub pull requests 125 | to merge their code. If you do this, then our 126 | [code testing](https://travis-ci.org/wafflesfm/batter) and 127 | [code coverage](https://coveralls.io/r/wafflesfm/batter) tools will 128 | automatically tell you if what you are about to merge is going to break 129 | everything, and will automatically remind you to write any necessary tests. 130 | -------------------------------------------------------------------------------- /batter/music/migrations/0002_auto__add_musicupload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'MusicUpload' 12 | db.create_table(u'music_musicupload', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 15 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 16 | ('release', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Release'])), 17 | ('torrent', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['torrents.Torrent'], unique=True)), 18 | )) 19 | db.send_create_signal(u'music', ['MusicUpload']) 20 | 21 | 22 | def backwards(self, orm): 23 | # Deleting model 'MusicUpload' 24 | db.delete_table(u'music_musicupload') 25 | 26 | 27 | models = { 28 | u'music.artist': { 29 | 'Meta': {'object_name': 'Artist'}, 30 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 31 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 32 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 33 | 'name': ('django.db.models.fields.TextField', [], {}), 34 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 35 | }, 36 | u'music.master': { 37 | 'Meta': {'object_name': 'Master'}, 38 | 'artists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['music.Artist']", 'null': 'True', 'blank': 'True'}), 39 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 40 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'main': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'to': u"orm['music.Release']"}), 42 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 43 | 'name': ('django.db.models.fields.TextField', [], {}), 44 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 45 | }, 46 | u'music.musicupload': { 47 | 'Meta': {'object_name': 'MusicUpload'}, 48 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 49 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 51 | 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Release']"}), 52 | 'torrent': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['torrents.Torrent']", 'unique': 'True'}) 53 | }, 54 | u'music.release': { 55 | 'Meta': {'object_name': 'Release'}, 56 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 57 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 58 | 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Master']", 'null': 'True', 'blank': 'True'}), 59 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 60 | 'name': ('django.db.models.fields.TextField', [], {}), 61 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 62 | }, 63 | u'torrents.torrent': { 64 | 'Meta': {'object_name': 'Torrent'}, 65 | 'announce': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 66 | 'announce_list': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 67 | 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 68 | 'created_by': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 69 | 'creation_date': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 70 | 'encoding': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 71 | 'files': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 72 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 74 | 'length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 75 | 'md5sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 76 | 'name': ('django.db.models.fields.TextField', [], {}), 77 | 'piece_length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 78 | 'pieces': ('django.db.models.fields.TextField', [], {'unique': 'True'}) 79 | } 80 | } 81 | 82 | complete_apps = ['music'] -------------------------------------------------------------------------------- /batter/music/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Artist' 12 | db.create_table(u'music_artist', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 15 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 16 | ('name', self.gf('django.db.models.fields.TextField')()), 17 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255)), 18 | )) 19 | db.send_create_signal(u'music', ['Artist']) 20 | 21 | # Adding model 'Master' 22 | db.create_table(u'music_master', ( 23 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 25 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 26 | ('name', self.gf('django.db.models.fields.TextField')()), 27 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255)), 28 | ('main', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name=u'+', null=True, to=orm['music.Release'])), 29 | )) 30 | db.send_create_signal(u'music', ['Master']) 31 | 32 | # Adding M2M table for field artists on 'Master' 33 | db.create_table(u'music_master_artists', ( 34 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 35 | ('master', models.ForeignKey(orm[u'music.master'], null=False)), 36 | ('artist', models.ForeignKey(orm[u'music.artist'], null=False)) 37 | )) 38 | db.create_unique(u'music_master_artists', ['master_id', 'artist_id']) 39 | 40 | # Adding model 'Release' 41 | db.create_table(u'music_release', ( 42 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 43 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 44 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 45 | ('name', self.gf('django.db.models.fields.TextField')()), 46 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255)), 47 | ('master', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Master'], null=True, blank=True)), 48 | )) 49 | db.send_create_signal(u'music', ['Release']) 50 | 51 | 52 | def backwards(self, orm): 53 | # Deleting model 'Artist' 54 | db.delete_table(u'music_artist') 55 | 56 | # Deleting model 'Master' 57 | db.delete_table(u'music_master') 58 | 59 | # Removing M2M table for field artists on 'Master' 60 | db.delete_table('music_master_artists') 61 | 62 | # Deleting model 'Release' 63 | db.delete_table(u'music_release') 64 | 65 | 66 | models = { 67 | u'music.artist': { 68 | 'Meta': {'object_name': 'Artist'}, 69 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 70 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 72 | 'name': ('django.db.models.fields.TextField', [], {}), 73 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 74 | }, 75 | u'music.master': { 76 | 'Meta': {'object_name': 'Master'}, 77 | 'artists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['music.Artist']", 'null': 'True', 'blank': 'True'}), 78 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 79 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'main': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'to': u"orm['music.Release']"}), 81 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 82 | 'name': ('django.db.models.fields.TextField', [], {}), 83 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 84 | }, 85 | u'music.release': { 86 | 'Meta': {'object_name': 'Release'}, 87 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 88 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 89 | 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Master']", 'null': 'True', 'blank': 'True'}), 90 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 91 | 'name': ('django.db.models.fields.TextField', [], {}), 92 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 93 | } 94 | } 95 | 96 | complete_apps = ['music'] -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\{{ project_name }}.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\{{ project_name }}.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /batter/notifications/migrations/0003_auto__add_field_notification_title_text__add_field_notification_body_t.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Notification.title_text' 12 | db.add_column(u'notifications_notification', 'title_text', 13 | self.gf('django.db.models.fields.TextField')(default='', blank=True), 14 | keep_default=False) 15 | 16 | # Adding field 'Notification.body_text' 17 | db.add_column(u'notifications_notification', 'body_text', 18 | self.gf('django.db.models.fields.TextField')(default='', blank=True), 19 | keep_default=False) 20 | 21 | 22 | # Changing field 'Notification.body' 23 | db.alter_column(u'notifications_notification', 'body', self.gf('django.db.models.fields.TextField')()) 24 | 25 | # Changing field 'Notification.title' 26 | db.alter_column(u'notifications_notification', 'title', self.gf('django.db.models.fields.TextField')()) 27 | 28 | def backwards(self, orm): 29 | # Deleting field 'Notification.title_text' 30 | db.delete_column(u'notifications_notification', 'title_text') 31 | 32 | # Deleting field 'Notification.body_text' 33 | db.delete_column(u'notifications_notification', 'body_text') 34 | 35 | 36 | # Changing field 'Notification.body' 37 | db.alter_column(u'notifications_notification', 'body', self.gf('django.db.models.fields.CharField')(max_length=512)) 38 | 39 | # Changing field 'Notification.title' 40 | db.alter_column(u'notifications_notification', 'title', self.gf('django.db.models.fields.CharField')(max_length=64)) 41 | 42 | models = { 43 | u'auth.group': { 44 | 'Meta': {'object_name': 'Group'}, 45 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 46 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 47 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 48 | }, 49 | u'auth.permission': { 50 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 51 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 52 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 53 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 55 | }, 56 | u'auth.user': { 57 | 'Meta': {'object_name': 'User'}, 58 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 59 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 60 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 61 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 62 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 64 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 65 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 66 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 67 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 68 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 69 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 70 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 71 | }, 72 | u'contenttypes.contenttype': { 73 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 74 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 75 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 76 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 77 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 78 | }, 79 | u'notifications.notification': { 80 | 'Meta': {'object_name': 'Notification'}, 81 | 'body': ('django.db.models.fields.TextField', [], {}), 82 | 'body_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 83 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notifications'", 'to': u"orm['auth.User']"}), 85 | 'seen': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 86 | 'seen_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), 87 | 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 88 | 'title': ('django.db.models.fields.TextField', [], {}), 89 | 'title_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) 90 | } 91 | } 92 | 93 | complete_apps = ['notifications'] -------------------------------------------------------------------------------- /batter/torrents/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import bencode 4 | import binascii 5 | from jsonfield import JSONField 6 | 7 | from django.db import models 8 | from django.core.urlresolvers import reverse 9 | from django.utils.encoding import python_2_unicode_compatible, force_bytes 10 | from django.utils.translation import ugettext as _ 11 | 12 | 13 | @python_2_unicode_compatible 14 | class Torrent(models.Model): 15 | announce = models.URLField(help_text=_("The announce URL of the tracker.")) 16 | announce_list = JSONField(blank=True, null=True) 17 | creation_date = models.PositiveIntegerField( 18 | blank=True, null=True, 19 | help_text=_("Torrent creation time in UNIX epoch format.")) 20 | comment = models.TextField( 21 | blank=True, null=True, 22 | help_text=_("Free-form textual comment of the torrent author.")) 23 | created_by = models.TextField( 24 | blank=True, null=True, 25 | help_text=_("Name and version of the program used to create the " 26 | "torrent.")) 27 | encoding = models.TextField( 28 | blank=True, null=True, 29 | help_text=_("Encoding used to generate the pieces part of the info " 30 | "dictionary in the torrent metadata")) 31 | piece_length = models.PositiveIntegerField( 32 | blank=True, null=True, 33 | help_text=_("Number of bytes in each piece")) 34 | pieces = models.TextField( 35 | unique=True, 36 | help_text=_("A concatenation of all 20-byte SHA1 hash values of the " 37 | "torrent's pieces")) 38 | is_private = models.BooleanField( 39 | help_text=_("Whether or not the client may obtain peer data from " 40 | "other sources (PEX, DHT).")) 41 | name = models.TextField( 42 | help_text=_("The suggested name of the torrent file, if single-file " 43 | "torrent, otherwise, the suggest name of the directory " 44 | "in which to put the files")) 45 | length = models.PositiveIntegerField( 46 | blank=True, null=True, 47 | help_text=_("Length of the file contents in bytes, missing for " 48 | "multi-file torrents.")) 49 | md5sum = models.CharField( 50 | blank=True, null=True, max_length=32, 51 | help_text=_("MD5 hash of the file contents (single-file torrent " 52 | " only).")) 53 | files = JSONField( 54 | blank=True, null=True, 55 | help_text=_("A list of {name, length, md5sum} dicts corresponding to " 56 | "the files tracked by the torrent")) 57 | 58 | def get_absolute_url(self): 59 | return reverse('torrents_torrent_download', args=[self.pk]) 60 | 61 | @classmethod 62 | def from_torrent_file(cls, torrent_file, *args, **kwargs): 63 | torrent_dict = bencode.bdecode(torrent_file.read()) 64 | return cls.from_torrent_dict(torrent_dict, *args, **kwargs) 65 | 66 | @classmethod 67 | def from_torrent_dict(cls, torrent_dict, *args, **kwargs): 68 | info_dict = torrent_dict[b'info'] 69 | return cls( 70 | announce=torrent_dict[b'announce'], 71 | announce_list=torrent_dict.get(b'announce-list'), 72 | creation_date=torrent_dict.get(b'creation date'), 73 | created_by=torrent_dict.get(b'created by'), 74 | comment=torrent_dict.get(b'comment'), 75 | encoding=torrent_dict.get(b'encoding'), 76 | piece_length=info_dict.get(b'piece length'), 77 | pieces=binascii.hexlify(info_dict.get(b'pieces')), 78 | is_private=info_dict.get(b'private', 0) == 1, 79 | name=info_dict.get(b'name'), 80 | length=info_dict.get(b'length'), 81 | md5sum=info_dict.get(b'md5sum'), 82 | files=info_dict.get(b'files')) 83 | 84 | @property 85 | def is_single_file(self): 86 | return self.files is None or len(self.files) <= 1 87 | 88 | def as_bencoded_string(self, *args, **kwargs): 89 | torrent = { 90 | 'announce': self.announce, 91 | 'announce-list': self.announce_list, 92 | 'creation date': self.creation_date, 93 | 'comment': self.comment, 94 | 'created by': self.created_by, 95 | 'encoding': self.encoding, 96 | } 97 | 98 | torrent['info'] = info_dict = { 99 | 'piece length': self.piece_length, 100 | 'pieces': binascii.unhexlify(self.pieces), 101 | 'private': int(self.is_private), 102 | 'name': self.name 103 | } 104 | if self.is_single_file: 105 | info_dict['length'] = self.length 106 | info_dict['md5sum'] = self.md5sum 107 | else: 108 | info_dict['files'] = self.files 109 | 110 | return bencode.bencode( 111 | recursive_force_bytes(recursive_drop_falsy(torrent))) 112 | 113 | def __str__(self): 114 | return self.name 115 | 116 | 117 | def recursive_drop_falsy(d): 118 | """Recursively drops falsy values from a given data structure.""" 119 | if isinstance(d, dict): 120 | return dict((k, recursive_drop_falsy(v)) for k, v in d.items() if v) 121 | elif isinstance(d, list): 122 | return map(recursive_drop_falsy, d) 123 | elif isinstance(d, basestring): 124 | return force_bytes(d) 125 | else: 126 | return d 127 | 128 | 129 | def recursive_force_bytes(d): 130 | """Recursively walks a given data structure and coerces all string-like 131 | values to :class:`bytes`.""" 132 | if isinstance(d, dict): 133 | # Note(superbobry): 'bencode' forces us to use byte keys. 134 | return dict((force_bytes(k), recursive_force_bytes(v)) 135 | for k, v in d.items() if v) 136 | elif isinstance(d, list): 137 | return map(recursive_force_bytes, d) 138 | elif isinstance(d, basestring): 139 | return force_bytes(d) 140 | else: 141 | return d 142 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/{{ project_name }}.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/{{ project_name }}.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/{{ project_name }}" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/{{ project_name }}" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /batter/music/migrations/0004_auto__del_field_master_label.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Deleting field 'Master.label' 12 | db.delete_column(u'music_master', 'label_id') 13 | 14 | 15 | def backwards(self, orm): 16 | # Adding field 'Master.label' 17 | db.add_column(u'music_master', 'label', 18 | self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Label'], null=True, blank=True), 19 | keep_default=False) 20 | 21 | 22 | models = { 23 | u'music.artist': { 24 | 'Meta': {'object_name': 'Artist'}, 25 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 26 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 28 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 29 | 'name': ('django.db.models.fields.TextField', [], {}), 30 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}), 31 | 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 32 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) 33 | }, 34 | u'music.label': { 35 | 'Meta': {'object_name': 'Label'}, 36 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 37 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 39 | 'name': ('django.db.models.fields.TextField', [], {}), 40 | 'parent_label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Label']", 'null': 'True', 'blank': 'True'}) 41 | }, 42 | u'music.master': { 43 | 'Meta': {'object_name': 'Master'}, 44 | 'artists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['music.Artist']", 'null': 'True', 'blank': 'True'}), 45 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 46 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 48 | 'main': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'to': u"orm['music.Release']"}), 49 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 50 | 'name': ('django.db.models.fields.TextField', [], {}), 51 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 52 | }, 53 | u'music.musicupload': { 54 | 'Meta': {'object_name': 'MusicUpload'}, 55 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 56 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 58 | 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Release']"}), 59 | 'torrent': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['torrents.Torrent']", 'unique': 'True'}) 60 | }, 61 | u'music.release': { 62 | 'Meta': {'object_name': 'Release'}, 63 | 'catalog_num': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 64 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 65 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Label']", 'null': 'True', 'blank': 'True'}), 67 | 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Master']"}), 68 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 69 | 'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 70 | 'scene': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) 72 | }, 73 | u'torrents.torrent': { 74 | 'Meta': {'object_name': 'Torrent'}, 75 | 'announce': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 76 | 'announce_list': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 77 | 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 78 | 'created_by': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 79 | 'creation_date': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 80 | 'encoding': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 81 | 'files': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 82 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 83 | 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 84 | 'length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 85 | 'md5sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 86 | 'name': ('django.db.models.fields.TextField', [], {}), 87 | 'piece_length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 88 | 'pieces': ('django.db.models.fields.TextField', [], {'unique': 'True'}) 89 | } 90 | } 91 | 92 | complete_apps = ['music'] -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # batter documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Feb 17 11:46:20 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'batter' 44 | copyright = u'2013, ChangeMyName' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'batterdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'batter.tex', u'batter Documentation', 187 | u'ChangeToMyName', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'batter', u'batter Documentation', 217 | [u'ChangeToMyName'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'batter', u'batter Documentation', 231 | u'ChangeToMyName', 'batter', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /batter/batter/settings/base.py: -------------------------------------------------------------------------------- 1 | """Common settings and globals.""" 2 | 3 | 4 | from os.path import abspath, basename, dirname, join, normpath 5 | from sys import path 6 | import os 7 | 8 | #url to login 9 | LOGIN_REDIRECT_URL = '/accounts/%(username)s/' 10 | LOGIN_URL = "/accounts/signin/" 11 | LOGOUT_URL = "/accounts/signout/" 12 | 13 | LOGIN_EXEMPT_URLS = ( 14 | r'^accounts/', # allow any URL under /account/* 15 | ) 16 | 17 | #tracker config 18 | TRACKERURL = os.environ.get('TRACKERURL', 'http://test.com/announce') 19 | 20 | ########## PATH CONFIGURATION 21 | # Absolute filesystem path to the Django project directory: 22 | DJANGO_ROOT = dirname(dirname(abspath(__file__))) 23 | 24 | # Absolute filesystem path to the top-level project folder: 25 | SITE_ROOT = dirname(DJANGO_ROOT) 26 | 27 | # Site name: 28 | SITE_NAME = basename(DJANGO_ROOT) 29 | 30 | # Add our project to our pythonpath, this way we don't need to type our project 31 | # name in our dotted import paths: 32 | path.append(DJANGO_ROOT) 33 | ########## END PATH CONFIGURATION 34 | 35 | 36 | ########## DEBUG CONFIGURATION 37 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug 38 | DEBUG = False 39 | 40 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug 41 | TEMPLATE_DEBUG = DEBUG 42 | ########## END DEBUG CONFIGURATION 43 | 44 | 45 | ########## MANAGER CONFIGURATION 46 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#admins 47 | ADMINS = ( 48 | ('Your Name', 'your_email@example.com'), 49 | ) 50 | 51 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#managers 52 | MANAGERS = ADMINS 53 | ########## END MANAGER CONFIGURATION 54 | 55 | 56 | ########## DATABASE CONFIGURATION 57 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases 58 | DATABASES = { 59 | 'default': { 60 | 'ENGINE': 'django.db.backends.', 61 | 'NAME': '', 62 | 'USER': '', 63 | 'PASSWORD': '', 64 | 'HOST': '', 65 | 'PORT': '', 66 | } 67 | } 68 | ########## END DATABASE CONFIGURATION 69 | 70 | 71 | ########## GENERAL CONFIGURATION 72 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#time-zone 73 | TIME_ZONE = 'America/Los_Angeles' 74 | 75 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code 76 | LANGUAGE_CODE = 'en-us' 77 | 78 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id 79 | SITE_ID = 1 80 | 81 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 82 | USE_I18N = True 83 | 84 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 85 | USE_L10N = True 86 | 87 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 88 | USE_TZ = True 89 | ########## END GENERAL CONFIGURATION 90 | 91 | 92 | ########## MEDIA CONFIGURATION 93 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root 94 | MEDIA_ROOT = normpath(join(SITE_ROOT, 'media')) 95 | 96 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url 97 | MEDIA_URL = '/media/' 98 | ########## END MEDIA CONFIGURATION 99 | 100 | 101 | ########## STATIC FILE CONFIGURATION 102 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root 103 | STATIC_ROOT = normpath(join(SITE_ROOT, 'assets')) 104 | 105 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url 106 | STATIC_URL = '/static/' 107 | 108 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/ 109 | # #std:setting-STATICFILES_DIRS 110 | STATICFILES_DIRS = ( 111 | normpath(join(SITE_ROOT, 'static')), 112 | ) 113 | 114 | # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/ 115 | # #staticfiles-finders 116 | STATICFILES_FINDERS = ( 117 | 'django.contrib.staticfiles.finders.FileSystemFinder', 118 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 119 | ) 120 | ########## END STATIC FILE CONFIGURATION 121 | 122 | 123 | ########## SECRET CONFIGURATION 124 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 125 | # Note: This key only used for development and testing. 126 | SECRET_KEY = r"9ledr(hq#a#r-sa8$l)5+3nila(h3pe5)+jvwdh8bbk%a+=!@-" 127 | ########## END SECRET CONFIGURATION 128 | 129 | 130 | ########## FIXTURE CONFIGURATION 131 | # See: https://docs.djangoproject.com/en/dev/ref/settings/ 132 | # #std:setting-FIXTURE_DIRS 133 | FIXTURE_DIRS = ( 134 | normpath(join(SITE_ROOT, 'fixtures')), 135 | ) 136 | ########## END FIXTURE CONFIGURATION 137 | 138 | 139 | ########## TEMPLATE CONFIGURATION 140 | # See: https://docs.djangoproject.com/en/dev/ref/settings/ 141 | # #template-context-processors 142 | TEMPLATE_CONTEXT_PROCESSORS = ( 143 | 'django.contrib.auth.context_processors.auth', 144 | 'django.core.context_processors.debug', 145 | 'django.core.context_processors.i18n', 146 | 'django.core.context_processors.media', 147 | 'django.core.context_processors.static', 148 | 'django.core.context_processors.tz', 149 | 'django.contrib.messages.context_processors.messages', 150 | 'django.core.context_processors.request', 151 | 'notifications.context_processors.notifications', 152 | ) 153 | 154 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 155 | TEMPLATE_LOADERS = ( 156 | 'hamlpy.template.loaders.HamlPyFilesystemLoader', 157 | 'hamlpy.template.loaders.HamlPyAppDirectoriesLoader', 158 | 'django.template.loaders.filesystem.Loader', 159 | 'django.template.loaders.app_directories.Loader', 160 | ) 161 | 162 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 163 | TEMPLATE_DIRS = ( 164 | normpath(join(SITE_ROOT, 'templates')), 165 | ) 166 | ########## END TEMPLATE CONFIGURATION 167 | 168 | 169 | ########## MIDDLEWARE CONFIGURATION 170 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes 171 | MIDDLEWARE_CLASSES = ( 172 | # Default Django middleware. 173 | 'django.middleware.common.CommonMiddleware', 174 | 'django.contrib.sessions.middleware.SessionMiddleware', 175 | 'django.middleware.csrf.CsrfViewMiddleware', 176 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 177 | 'django.contrib.messages.middleware.MessageMiddleware', 178 | 179 | 'userena.middleware.UserenaLocaleMiddleware', 180 | 181 | 'batter.middleware.LoginRequiredMiddleware', 182 | ) 183 | ########## END MIDDLEWARE CONFIGURATION 184 | 185 | 186 | ########## URL CONFIGURATION 187 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 188 | ROOT_URLCONF = '%s.urls' % SITE_NAME 189 | ########## END URL CONFIGURATION 190 | 191 | 192 | ########## APP CONFIGURATION 193 | DJANGO_APPS = ( 194 | # Default Django apps: 195 | 'django.contrib.auth', 196 | 'django.contrib.contenttypes', 197 | 'django.contrib.sessions', 198 | 'django.contrib.sites', 199 | 'django.contrib.messages', 200 | 'django.contrib.staticfiles', 201 | 202 | # Useful template tags: 203 | 'django.contrib.humanize', 204 | 205 | # Third-party app, but needs to be declared before d.c.admin. 206 | # See https://django-grappelli.readthedocs.org/en/2.4.5/quickstart.html 207 | 'grappelli', 208 | 209 | # Admin panel and documentation: 210 | 'django.contrib.admin', 211 | # 'django.contrib.admindocs', 212 | ) 213 | 214 | THIRD_PARTY_APPS = ( 215 | 'django_extensions', 216 | 'django_forms_bootstrap', 217 | 'notification', 218 | 'south', 219 | 'haystack', 220 | 'widget_tweaks', 221 | # Per-object permission system 222 | 'guardian', 223 | 'easy_thumbnails', 224 | 'userena' 225 | ) 226 | 227 | # Apps specific for this project go here. 228 | LOCAL_APPS = ( 229 | 'batter', 230 | 'core', 231 | 'music', 232 | 'notifications', 233 | 'profiles', 234 | 'torrents', 235 | ) 236 | 237 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 238 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 239 | ########## END APP CONFIGURATION 240 | 241 | 242 | AUTH_PROFILE_MODULE = 'core.UserProfile' 243 | 244 | 245 | AUTHENTICATION_BACKENDS = ( 246 | 'userena.backends.UserenaAuthenticationBackend', 247 | 'guardian.backends.ObjectPermissionBackend', 248 | 'django.contrib.auth.backends.ModelBackend', 249 | ) 250 | 251 | 252 | ANONYMOUS_USER_ID = -1 253 | 254 | 255 | ########## LOGGING CONFIGURATION 256 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#logging 257 | # A sample logging configuration. The only tangible logging 258 | # performed by this configuration is to send an email to 259 | # the site admins on every HTTP 500 error when DEBUG=False. 260 | # See http://docs.djangoproject.com/en/dev/topics/logging for 261 | # more details on how to customize your logging configuration. 262 | LOGGING = { 263 | 'version': 1, 264 | 'disable_existing_loggers': False, 265 | 'filters': { 266 | 'require_debug_false': { 267 | '()': 'django.utils.log.RequireDebugFalse' 268 | } 269 | }, 270 | 'handlers': { 271 | 'mail_admins': { 272 | 'level': 'ERROR', 273 | 'filters': ['require_debug_false'], 274 | 'class': 'django.utils.log.AdminEmailHandler' 275 | } 276 | }, 277 | 'loggers': { 278 | 'django.request': { 279 | 'handlers': ['mail_admins'], 280 | 'level': 'ERROR', 281 | 'propagate': True, 282 | }, 283 | } 284 | } 285 | ########## END LOGGING CONFIGURATION 286 | 287 | 288 | ########## WSGI CONFIGURATION 289 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 290 | WSGI_APPLICATION = 'wsgi.application' 291 | ########## END WSGI CONFIGURATION 292 | 293 | ########## django-notification CONFIGURATION 294 | NOTIFICATION_BACKENDS = [ 295 | ("email", "notification.backends.email.EmailBackend"), 296 | ("model", "notifications.backend.ModelBackend"), 297 | ] 298 | ########## END django-notification CONFIGURATION 299 | -------------------------------------------------------------------------------- /batter/music/migrations/0003_auto__add_label__add_field_artist_image__add_field_artist_summary__add.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Label' 12 | db.create_table(u'music_label', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 15 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 16 | ('name', self.gf('django.db.models.fields.TextField')()), 17 | ('parent_label', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Label'], null=True, blank=True)), 18 | )) 19 | db.send_create_signal(u'music', ['Label']) 20 | 21 | # Adding field 'Artist.image' 22 | db.add_column(u'music_artist', 'image', 23 | self.gf('django.db.models.fields.files.ImageField')(max_length=100, null=True, blank=True), 24 | keep_default=False) 25 | 26 | # Adding field 'Artist.summary' 27 | db.add_column(u'music_artist', 'summary', 28 | self.gf('django.db.models.fields.TextField')(default='', blank=True), 29 | keep_default=False) 30 | 31 | # Adding field 'Artist.url' 32 | db.add_column(u'music_artist', 'url', 33 | self.gf('django.db.models.fields.URLField')(default='', max_length=200, blank=True), 34 | keep_default=False) 35 | 36 | # Adding field 'Master.image' 37 | db.add_column(u'music_master', 'image', 38 | self.gf('django.db.models.fields.files.ImageField')(max_length=100, null=True, blank=True), 39 | keep_default=False) 40 | 41 | # Adding field 'Master.label' 42 | db.add_column(u'music_master', 'label', 43 | self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Label'], null=True, blank=True), 44 | keep_default=False) 45 | 46 | # Deleting field 'Release.slug' 47 | db.delete_column(u'music_release', 'slug') 48 | 49 | # Adding field 'Release.label' 50 | db.add_column(u'music_release', 'label', 51 | self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Label'], null=True, blank=True), 52 | keep_default=False) 53 | 54 | # Adding field 'Release.year' 55 | db.add_column(u'music_release', 'year', 56 | self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True), 57 | keep_default=False) 58 | 59 | # Adding field 'Release.catalog_num' 60 | db.add_column(u'music_release', 'catalog_num', 61 | self.gf('django.db.models.fields.TextField')(default='', blank=True), 62 | keep_default=False) 63 | 64 | # Adding field 'Release.scene' 65 | db.add_column(u'music_release', 'scene', 66 | self.gf('django.db.models.fields.BooleanField')(default=False), 67 | keep_default=False) 68 | 69 | 70 | # Changing field 'Release.master' 71 | db.alter_column(u'music_release', 'master_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['music.Master'])) 72 | 73 | def backwards(self, orm): 74 | # Deleting model 'Label' 75 | db.delete_table(u'music_label') 76 | 77 | # Deleting field 'Artist.image' 78 | db.delete_column(u'music_artist', 'image') 79 | 80 | # Deleting field 'Artist.summary' 81 | db.delete_column(u'music_artist', 'summary') 82 | 83 | # Deleting field 'Artist.url' 84 | db.delete_column(u'music_artist', 'url') 85 | 86 | # Deleting field 'Master.image' 87 | db.delete_column(u'music_master', 'image') 88 | 89 | # Deleting field 'Master.label' 90 | db.delete_column(u'music_master', 'label_id') 91 | 92 | # Adding field 'Release.slug' 93 | db.add_column(u'music_release', 'slug', 94 | self.gf('django.db.models.fields.SlugField')(default='release', max_length=255), 95 | keep_default=False) 96 | 97 | # Deleting field 'Release.label' 98 | db.delete_column(u'music_release', 'label_id') 99 | 100 | # Deleting field 'Release.year' 101 | db.delete_column(u'music_release', 'year') 102 | 103 | # Deleting field 'Release.catalog_num' 104 | db.delete_column(u'music_release', 'catalog_num') 105 | 106 | # Deleting field 'Release.scene' 107 | db.delete_column(u'music_release', 'scene') 108 | 109 | 110 | # Changing field 'Release.master' 111 | db.alter_column(u'music_release', 'master_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['music.Master'], null=True)) 112 | 113 | models = { 114 | u'music.artist': { 115 | 'Meta': {'object_name': 'Artist'}, 116 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 117 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 118 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 119 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 120 | 'name': ('django.db.models.fields.TextField', [], {}), 121 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}), 122 | 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 123 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) 124 | }, 125 | u'music.label': { 126 | 'Meta': {'object_name': 'Label'}, 127 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 128 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 129 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 130 | 'name': ('django.db.models.fields.TextField', [], {}), 131 | 'parent_label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Label']", 'null': 'True', 'blank': 'True'}) 132 | }, 133 | u'music.master': { 134 | 'Meta': {'object_name': 'Master'}, 135 | 'artists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['music.Artist']", 'null': 'True', 'blank': 'True'}), 136 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 137 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 138 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 139 | 'label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Label']", 'null': 'True', 'blank': 'True'}), 140 | 'main': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'to': u"orm['music.Release']"}), 141 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 142 | 'name': ('django.db.models.fields.TextField', [], {}), 143 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) 144 | }, 145 | u'music.musicupload': { 146 | 'Meta': {'object_name': 'MusicUpload'}, 147 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 148 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 149 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 150 | 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Release']"}), 151 | 'torrent': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['torrents.Torrent']", 'unique': 'True'}) 152 | }, 153 | u'music.release': { 154 | 'Meta': {'object_name': 'Release'}, 155 | 'catalog_num': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 156 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 157 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 158 | 'label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Label']", 'null': 'True', 'blank': 'True'}), 159 | 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['music.Master']"}), 160 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 161 | 'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 162 | 'scene': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 163 | 'year': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) 164 | }, 165 | u'torrents.torrent': { 166 | 'Meta': {'object_name': 'Torrent'}, 167 | 'announce': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 168 | 'announce_list': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 169 | 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 170 | 'created_by': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 171 | 'creation_date': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 172 | 'encoding': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 173 | 'files': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 174 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 175 | 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 176 | 'length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 177 | 'md5sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 178 | 'name': ('django.db.models.fields.TextField', [], {}), 179 | 'piece_length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 180 | 'pieces': ('django.db.models.fields.TextField', [], {'unique': 'True'}) 181 | } 182 | } 183 | 184 | complete_apps = ['music'] -------------------------------------------------------------------------------- /batter/torrents/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Torrent' 12 | db.create_table(u'torrents_torrent', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('announce', self.gf('django.db.models.fields.URLField')(max_length=200)), 15 | ('announce_list', self.gf('jsonfield.fields.JSONField')(null=True, blank=True)), 16 | ('creation_date', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), 17 | ('comment', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 18 | ('created_by', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 19 | ('encoding', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 20 | ('piece_length', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), 21 | ('pieces', self.gf('django.db.models.fields.TextField')(unique=True)), 22 | ('is_private', self.gf('django.db.models.fields.BooleanField')(default=False)), 23 | ('name', self.gf('django.db.models.fields.TextField')()), 24 | ('length', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), 25 | ('md5sum', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)), 26 | ('files', self.gf('jsonfield.fields.JSONField')(null=True, blank=True)), 27 | )) 28 | db.send_create_signal(u'torrents', ['Torrent']) 29 | 30 | # Adding model 'Upload' 31 | db.create_table(u'torrents_upload', ( 32 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 33 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 34 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 35 | ('_subclass_name', self.gf('django.db.models.fields.CharField')(max_length=100)), 36 | ('torrent', self.gf('django.db.models.fields.related.OneToOneField')(related_name=u'upload', unique=True, to=orm['torrents.Torrent'])), 37 | ('uploader', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 38 | ('parent_content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), 39 | ('parent_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 40 | ('upload_group', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'uploads', to=orm['torrents.UploadGroup'])), 41 | )) 42 | db.send_create_signal(u'torrents', ['Upload']) 43 | 44 | # Adding model 'UploadGroup' 45 | db.create_table(u'torrents_uploadgroup', ( 46 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 47 | ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), 48 | ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), 49 | ('_subclass_name', self.gf('django.db.models.fields.CharField')(max_length=100)), 50 | )) 51 | db.send_create_signal(u'torrents', ['UploadGroup']) 52 | 53 | 54 | def backwards(self, orm): 55 | # Deleting model 'Torrent' 56 | db.delete_table(u'torrents_torrent') 57 | 58 | # Deleting model 'Upload' 59 | db.delete_table(u'torrents_upload') 60 | 61 | # Deleting model 'UploadGroup' 62 | db.delete_table(u'torrents_uploadgroup') 63 | 64 | 65 | models = { 66 | u'auth.group': { 67 | 'Meta': {'object_name': 'Group'}, 68 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 70 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 71 | }, 72 | u'auth.permission': { 73 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 74 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 75 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 76 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 78 | }, 79 | u'auth.user': { 80 | 'Meta': {'object_name': 'User'}, 81 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 82 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 83 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 84 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 85 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 87 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 88 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 89 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 90 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 91 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 92 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 93 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 94 | }, 95 | u'contenttypes.contenttype': { 96 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 97 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 98 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 99 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 100 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 101 | }, 102 | u'taggit.tag': { 103 | 'Meta': {'object_name': 'Tag'}, 104 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 105 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), 106 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) 107 | }, 108 | u'taggit.taggeditem': { 109 | 'Meta': {'object_name': 'TaggedItem'}, 110 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), 111 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 112 | 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 113 | 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) 114 | }, 115 | u'torrents.torrent': { 116 | 'Meta': {'object_name': 'Torrent'}, 117 | 'announce': ('django.db.models.fields.URLField', [], {'max_length': '200'}), 118 | 'announce_list': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 119 | 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 120 | 'created_by': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 121 | 'creation_date': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 122 | 'encoding': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 123 | 'files': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), 124 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 125 | 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 126 | 'length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 127 | 'md5sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), 128 | 'name': ('django.db.models.fields.TextField', [], {}), 129 | 'piece_length': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 130 | 'pieces': ('django.db.models.fields.TextField', [], {'unique': 'True'}) 131 | }, 132 | u'torrents.upload': { 133 | 'Meta': {'object_name': 'Upload'}, 134 | '_subclass_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 135 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 136 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 137 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 138 | 'parent_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 139 | 'parent_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 140 | 'torrent': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "u'upload'", 'unique': 'True', 'to': u"orm['torrents.Torrent']"}), 141 | 'upload_group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'uploads'", 'to': u"orm['torrents.UploadGroup']"}), 142 | 'uploader': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) 143 | }, 144 | u'torrents.uploadgroup': { 145 | 'Meta': {'object_name': 'UploadGroup'}, 146 | '_subclass_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 147 | 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 148 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 149 | 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}) 150 | } 151 | } 152 | 153 | complete_apps = ['torrents'] -------------------------------------------------------------------------------- /batter/static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2013 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | --------------------------------------------------------------------------------