├── 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 |
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 | [](https://travis-ci.org/wafflesfm/batter) [](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 |
--------------------------------------------------------------------------------