├── .gitignore ├── AUTHORS ├── Changelog ├── INSTALL ├── MANIFEST.in ├── README ├── README.rst ├── TODO ├── djintegration ├── __init__.py ├── admin.py ├── backends.py ├── commands.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── forcetestreports.py │ │ └── maketestreports.py ├── models.py ├── settings.py ├── static │ └── djintegration │ │ ├── main.css │ │ └── main.js ├── tasks.py ├── templates │ └── djintegration │ │ ├── base.html │ │ ├── error_email.html │ │ ├── latest_reports.html │ │ ├── repository.html │ │ └── repository_partial.html ├── templatetags │ ├── __init__.py │ └── djintegration_tags.py ├── tests │ ├── __init__.py │ ├── gitlog.txt │ ├── hglog.txt │ ├── source_control.py │ └── svninfo.txt ├── urls.py └── views.py ├── docs ├── Makefile ├── _ext │ ├── applyxrefs.py │ └── literals_to_xrefs.py ├── _theme │ └── nature │ │ ├── static │ │ ├── nature.css_t │ │ └── pygments.css │ │ └── theme.conf ├── changelog.rst ├── conf.py ├── index.rst ├── introduction.rst └── shot.png ├── requirements.txt ├── setup.cfg ├── setup.py └── testproj ├── __init__.py ├── manage.py ├── middleware.py ├── settings.py ├── templates └── admin │ └── base_site.html └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *~ 4 | *.sqlite 5 | *.sqlite-journal 6 | *.build 7 | djintegration.db 8 | settings_local.py 9 | local_settings.py 10 | .*.sw[po] 11 | dist/ 12 | *.egg-info 13 | doc/__build/* 14 | build/ 15 | locale/ 16 | pip-log.txt 17 | devdatabase.db 18 | .directory 19 | bundle_version.gen 20 | celeryd.log 21 | celeryd.pid 22 | testproj/settings.py 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Batiste Bieler 2 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2 | CHANGELOG 3 | ============= 4 | 5 | 6 | 7 | Added a column the install commands:: 8 | 9 | ALTER TABLE djintegration_repository ADD COLUMN virtual_env_type varchar(16) default "vs"; 10 | 11 | Added a column the install commands:: 12 | 13 | ALTER TABLE djintegration_repository ADD COLUMN install_command text default ""; 14 | 15 | Added a column for overrding email notifications:: 16 | 17 | ALTER TABLE djintegration_repository ADD COLUMN emails text NOT NULL default ""; 18 | 19 | Added a column for state on the test report:: 20 | 21 | ALTER TABLE djintegration_testreport ADD COLUMN state varchar(10) NOT NULL DEFAULT "pass"; 22 | 23 | Added a column the install output:: 24 | 25 | ALTER TABLE djintegration_testreport ADD COLUMN install text default ""; 26 | 27 | Added a column for caching the state of the repo:: 28 | 29 | ALTER TABLE djintegration_repository ADD COLUMN state VARCHAR(10) DEFAULT "pass"; 30 | 31 | Added a column for commit author:: 32 | 33 | ALTER TABLE "djintegration_testreport" ADD COLUMN "author" varchar(100) NOT NULL DEFAULT ""; 34 | 35 | Added a column for being language agnostic:: 36 | 37 | ALTER TABLE "djintegration_repository" ADD COLUMN "test_command" text; -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install ``djintegration`` either via the Python Package Index (PyPI) 5 | or from source. 6 | 7 | To install using ``pip``,:: 8 | 9 | $ pip install djintegration 10 | 11 | 12 | To install using ``easy_install``,:: 13 | 14 | $ easy_install djintegration 15 | 16 | 17 | If you have downloaded a source tarball you can install it 18 | by doing the following,:: 19 | 20 | $ python setup.py build 21 | # python setup.py install # as root 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include Changelog 3 | include INSTALL 4 | include MANIFEST.in 5 | include README 6 | include TODO 7 | recursive-include docs * 8 | recursive-include djintegration * 9 | recursive-include tests * 10 | recursive-include djintegration/templates * 11 | global-include *.html 12 | global-include *.txt 13 | global-exclude *.pyc 14 | prune testproj -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================================================================ 2 | djintegration - Continuous integration reports for python 3 | ============================================================================ 4 | 5 | Introduction 6 | ============== 7 | 8 | Django continuous integration is a tool for running and displaying 9 | tests for the python language. Although python and Django are used, 10 | this tools is meant to be used with any python project that has tests. 11 | 12 | Django continuous integration currently support `Git`, `Subversion` and `Mercurial`. 13 | 14 | .. image:: https://raw.github.com/batiste/django-continuous-integration/master/docs/shot.png 15 | 16 | 17 | How does it work 18 | ================= 19 | 20 | You need to create a repository within the admin by providing a repository URL 21 | and add a test command (the default being `"python setup.py test"`). 22 | 23 | For every test this software create a virtual python environnement (virtualenv) 24 | and try to setup your python application by checking out your code and installing 25 | the necesseray dependencies. It's nececessary that your package list all 26 | the needed dependencies in `setup.py`. It's possible that you will have to 27 | install some distribution dependencies by hand (like the developement headers for lxml). 28 | 29 | Then you can generate a test report for all your repositories 30 | using the `djintegration.commands.make_test_reports` commands 31 | or by using the manage command:: 32 | 33 | $ cd testproj/ 34 | $ python manage.py maketestreports 35 | 36 | This command will checkout your repositories in "/tmp/" and try to 37 | execute the command you provided. The result will be stored in the 38 | test report model. 39 | 40 | The success or failure of the test is determined by the return code 41 | of the test command. 42 | 43 | Using buildout 44 | =============== 45 | 46 | If you use builout with your project, you will not need any virtual environnement. 47 | Choose "No virtual environnement" in the admin options. 48 | Then you can execute your builout commands within the install textarea:: 49 | 50 | python /bootstrap.py --distribute 51 | ./bin/buildout -v 52 | 53 | Coverage support 54 | ================= 55 | 56 | If you support coverage in your tests, Django continuous will search for the coverage HTML directory. 57 | The name of the directory has to be `covhtml` or `htmlcov`. You can override this setting:: 58 | 59 | DJANGO_INTEGRATION_COV_CANDIDATES = ['htmlcov', 'covhtml', '...'] 60 | 61 | If found, the coverage HTML directory will be served via the web interface. 62 | 63 | 64 | Settings 65 | =========== 66 | 67 | DJANGO_INTEGRATION_MAILS 68 | --------------------------- 69 | 70 | Default value: [] 71 | 72 | A list of emails where the failing tests are sent. You can override this value within the administration. 73 | 74 | DJANGO_INTEGRATION_FROM_MAIL 75 | ------------------------------ 76 | 77 | Default value: "django-continuous-integration@noreply.com" 78 | 79 | "From" field for report emails. 80 | 81 | DJANGO_INTEGRATION_MAIL_TITLE 82 | ------------------------------- 83 | 84 | Default value: "%s latest tests didn\'t passed" 85 | 86 | "Title" field for report emails, `%s` is the repository `URL`. 87 | 88 | DJANGO_INTEGRATION_DIRECTORY 89 | -------------------------------- 90 | 91 | Default value: "/tmp/" 92 | 93 | Directory where the virtualenv will be created and test runned. 94 | 95 | 96 | What it doesn't do (yet) 97 | ========================= 98 | 99 | This package is not a client/server architecture yet. All the tests are run 100 | on the server. 101 | 102 | Make it run automaticaly 103 | ========================== 104 | 105 | As you could have multiple repositories in different locations, I think polling is a 106 | realisitic approch. For that ou could use a cron job to make it run every 10 minutes:: 107 | 108 | */10 * * * * cd /project/directory/;python manage.py maketestreports >> reports.log 109 | 110 | Get started 111 | ============= 112 | 113 | Like all Django application you need to syncdb:: 114 | 115 | $ cd testproj/ 116 | $ python manage.py syncdb 117 | $ python manage.py runserver 118 | 119 | Then go the administration interface and add your repositories. 120 | 121 | 122 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | See http://github.com/batiste/django-continuous-integration/issues 2 | -------------------------------------------------------------------------------- /djintegration/__init__.py: -------------------------------------------------------------------------------- 1 | """Django continuous integration application.""" 2 | VERSION = (0, 0, 1) 3 | __version__ = ".".join(map(str, VERSION)) 4 | __author__ = "Opera Software (WebTeam)" 5 | __contact__ = "batisteb@opera.com" 6 | __homepage__ = "http://github.com/batiste/django-continuous-integration" 7 | __docformat__ = "restructuredtext" 8 | -------------------------------------------------------------------------------- /djintegration/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from os.path import join 3 | 4 | from django.contrib import admin 5 | from django.db import models 6 | from django.conf import settings 7 | 8 | from djintegration.models import Repository, TestReport 9 | 10 | admin.site.register(TestReport) 11 | admin.site.register(Repository) 12 | -------------------------------------------------------------------------------- /djintegration/backends.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import os 4 | import sys 5 | import shlex 6 | import shutil 7 | from subprocess import Popen, PIPE, STDOUT 8 | from datetime import datetime, timedelta 9 | 10 | from djintegration.models import TestReport, Repository 11 | from djintegration.settings import INT_DIR, COV_CANDIDATES, TESTED_APP_DIR 12 | from djintegration.settings import MAX_RUNNING_TIME 13 | 14 | MAX_RUN_DELTA = timedelta(seconds=MAX_RUNNING_TIME) 15 | 16 | 17 | def with_dir(dirname, fun): 18 | cwd = os.getcwd() 19 | result = None 20 | try: 21 | os.chdir(dirname) 22 | result = fun() 23 | finally: 24 | os.chdir(cwd) 25 | return result 26 | 27 | def line_that_starts_with(log, start): 28 | for line in log.split('\n'): 29 | if line.startswith(start): 30 | return line.split(start)[1].strip() 31 | 32 | def is_virtualenv_command(cmd): 33 | if (cmd.startswith('easy_install') or 34 | cmd.startswith('pip') or 35 | cmd.startswith('python')): 36 | return True 37 | else: 38 | return False 39 | 40 | class RepoBackend(object): 41 | 42 | use_virtualenv = False 43 | checkout_cmd = None 44 | update_cmd = None 45 | 46 | def __init__(self, repo, *args, **kwargs): 47 | self.repo = repo 48 | 49 | def system(self, commands): 50 | commands = str(commands) 51 | print "commands:", commands 52 | 53 | if ";" in commands: 54 | ret = os.system(commands) 55 | return "", ret 56 | else: 57 | args = shlex.split(commands) 58 | process = Popen(args, stdout=PIPE, stderr=STDOUT) 59 | output, errors = process.communicate() 60 | return output, process.returncode 61 | 62 | def dirname(self): 63 | return os.path.join(INT_DIR, self.repo.dirname() + '/') 64 | 65 | def app_dirname(self): 66 | return os.path.join(self.dirname(), TESTED_APP_DIR) 67 | 68 | def cov_dirname(self): 69 | return os.path.join(INT_DIR, self.repo.dirname() + '-cov/') 70 | 71 | def command_env(self, cmd): 72 | if is_virtualenv_command(cmd): 73 | cmd = 'bin/' + cmd 74 | def _command(): 75 | return self.system(cmd) 76 | return with_dir(self.dirname(), _command) 77 | 78 | 79 | 80 | def command_app(self, cmd, use_test_subpath=False): 81 | if is_virtualenv_command(cmd): 82 | cmd = '../bin/' + cmd 83 | def _command(): 84 | return self.system(cmd) 85 | command_dir = self.dirname() + TESTED_APP_DIR 86 | if use_test_subpath: 87 | command_dir += '/' + self.repo.test_subpath 88 | return with_dir(command_dir, _command) 89 | 90 | def setup_env(self): 91 | # trash the old virtual env 92 | try: 93 | shutil.rmtree(self.dirname()) 94 | except OSError: 95 | pass 96 | 97 | virtual_env_commands = { 98 | 'vs':'virtualenv --no-site-packages %s', 99 | 'vd':'virtualenv --no-site-packages %s --distribute' 100 | } 101 | cmd = virtual_env_commands.get(self.repo.virtual_env_type, None) 102 | 103 | if cmd: 104 | self.system(cmd % self.dirname()) 105 | self.use_virtualenv = True 106 | else: 107 | if not os.path.isdir(self.dirname()): 108 | os.makedirs(self.dirname()) 109 | 110 | # it seems to be the only thing that work properly 111 | # ./bin/activate fails 112 | activate_this = self.dirname() + 'bin/activate_this.py' 113 | execfile(activate_this, dict(__file__=activate_this)) 114 | 115 | return self.command_env(self.checkout_cmd % 116 | (self.repo.url, 'tested_app')) 117 | 118 | def run_tests(self): 119 | cmds = self.repo.get_test_command() 120 | cmds = cmds.replace('\r\n', ';;') 121 | log = "" 122 | for cmd in cmds.split(';;'): 123 | if len(cmd): 124 | out, ret = self.command_app(cmd, use_test_subpath=True) 125 | log = log + out 126 | return (log, ret) 127 | 128 | def install(self): 129 | cmds = self.repo.get_install_command() 130 | cmds = cmds.replace('\r\n', ';;') 131 | log = "" 132 | for cmd in cmds.split(';;'): 133 | if len(cmd): 134 | out, ret = self.command_app(cmd, use_test_subpath=True) 135 | log = log + out 136 | return (log, ret) 137 | 138 | def teardown_env(self): 139 | # search for coverage results directory 140 | cov_dir = None 141 | for directories in os.walk(self.app_dirname()): 142 | for candidate in COV_CANDIDATES: 143 | if candidate in directories[1]: 144 | cov_dir = os.path.join(directories[0], candidate) 145 | if cov_dir: 146 | try: 147 | shutil.rmtree(self.cov_dirname()) 148 | except OSError: 149 | pass 150 | shutil.move(cov_dir, self.cov_dirname()) 151 | 152 | def make_report(self, force=False): 153 | 154 | report = self.repo.last_test_report() 155 | if report: 156 | delta = datetime.now() - report.creation_date 157 | if report.state == "running" and delta < MAX_RUN_DELTA: 158 | print "Last report is still running, running time is %s" % str(delta) 159 | return report 160 | 161 | self.setup_env() 162 | commit = self.last_commit() 163 | new_test = None 164 | 165 | if self.repo.last_commit == commit: 166 | print "No new commit since last test." 167 | 168 | if force or self.repo.last_commit != commit or len(commit) == 0: 169 | 170 | author = self.last_commit_author() 171 | 172 | new_test = TestReport( 173 | repository=self.repo, 174 | install="Running ...", 175 | result="Running ...", 176 | commit=commit, 177 | author=author, 178 | state='running', 179 | ) 180 | new_test.save() 181 | 182 | try: 183 | install_result, returncodeinstall = self.install() 184 | # mysql text field has a limitation on how large a text field can be 185 | # 65,535 bytes ~64kb 186 | mysql_text_limit = 40000 187 | install_result = install_result[-mysql_text_limit:] 188 | 189 | test_result, returncode = self.run_tests() 190 | print test_result 191 | test_result = test_result[-mysql_text_limit:] 192 | 193 | except Exception as e: 194 | returncode = 1 195 | install_result = str(e) 196 | test_result = str(e) 197 | 198 | if returncode == 0: 199 | result_state = "pass" 200 | else: 201 | result_state = "fail" 202 | new_test.install=install_result 203 | new_test.result=test_result 204 | new_test.state=result_state 205 | new_test.save() 206 | 207 | # this to avoid to override things that could have been modified in 208 | # the admin, we fetch the repo again. 209 | save_repo = Repository.objects.get(pk=self.repo.pk) 210 | save_repo.state = result_state 211 | save_repo.last_commit = commit 212 | save_repo.save() 213 | 214 | self.teardown_env() 215 | return new_test 216 | 217 | 218 | def last_commit(self): 219 | log, returncode = self.command_app(self.log_cmd) 220 | return self.get_commit(log) 221 | 222 | def last_commit_author(self): 223 | log, returncode = self.command_app(self.log_cmd) 224 | return self.get_author(log) 225 | 226 | class GitBackend(RepoBackend): 227 | 228 | checkout_cmd = 'git clone %s %s' 229 | update_cmd = 'git pull' 230 | log_cmd = 'git log -n 1' 231 | 232 | def get_commit(self, log): 233 | return line_that_starts_with(log, 'commit ') 234 | 235 | def get_author(self, log): 236 | return line_that_starts_with(log, 'Author: ') 237 | 238 | class SvnBackend(RepoBackend): 239 | 240 | checkout_cmd = 'svn checkout %s %s' 241 | update_cmd = 'svn up' 242 | log_cmd = 'svn info' 243 | 244 | def get_commit(self, log): 245 | return line_that_starts_with(log, 'Revision: ') 246 | 247 | def get_author(self, log): 248 | return line_that_starts_with(log, 'Last Changed Author: ') 249 | 250 | 251 | class MercurialBackend(RepoBackend): 252 | 253 | checkout_cmd = 'hg clone %s %s' 254 | update_cmd = 'hg pull' 255 | log_cmd = 'hg log --limit 1' 256 | 257 | def get_commit(self, log): 258 | return line_that_starts_with(log, 'changeset: ') 259 | 260 | def get_author(self, log): 261 | return line_that_starts_with(log, 'user: ') 262 | 263 | 264 | def get_backend(repo): 265 | if repo.type == 'git': 266 | return GitBackend(repo) 267 | elif repo.type == 'svn': 268 | return SvnBackend(repo) 269 | elif repo.type == 'hg': 270 | return MercurialBackend(repo) 271 | 272 | raise NotImplementedError("Unsuppoted backend %s", repo.type) -------------------------------------------------------------------------------- /djintegration/commands.py: -------------------------------------------------------------------------------- 1 | 2 | from djintegration.models import Repository, TestReport 3 | from djintegration.backends import get_backend 4 | from django.core.mail import send_mail 5 | from django.template.loader import render_to_string 6 | from django.conf import settings 7 | 8 | EMAILS = getattr(settings, 'DJANGO_INTEGRATION_MAILS', []) 9 | FROM = getattr(settings, 'DJANGO_INTEGRATION_FROM_MAIL', 10 | 'django-continuous-integration@noreply.com') 11 | TITLE = getattr(settings, 'DJANGO_INTEGRATION_MAIL_TITLE', 12 | '%s latest tests didn\'t passed') 13 | 14 | 15 | def make_test_reports(force=False): 16 | 17 | tests_to_report = [] 18 | 19 | for repo in Repository.objects.all(): 20 | print "Making test report for %s (%s)" % (repo.name, repo.url) 21 | backend = get_backend(repo) 22 | 23 | new_test = backend.make_report(force) 24 | if new_test and new_test.fail(): 25 | tests_to_report.append(new_test) 26 | 27 | 28 | for test in tests_to_report: 29 | 30 | repo = test.repository 31 | 32 | if repo.emails: 33 | emails = repo.emails.split(',') 34 | else: 35 | emails = EMAILS 36 | 37 | title = TITLE % repo.url 38 | message = render_to_string('djintegration/error_email.html', 39 | {'test':test}) 40 | 41 | send_mail( 42 | title, 43 | message, 44 | FROM, 45 | emails, 46 | fail_silently=True 47 | ) 48 | -------------------------------------------------------------------------------- /djintegration/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-continuous-integration/7ba39cac31592448bc516df023e49517aa6ad400/djintegration/management/__init__.py -------------------------------------------------------------------------------- /djintegration/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-continuous-integration/7ba39cac31592448bc516df023e49517aa6ad400/djintegration/management/commands/__init__.py -------------------------------------------------------------------------------- /djintegration/management/commands/forcetestreports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.management.base import NoArgsCommand 3 | from django.conf import settings 4 | from djintegration.commands import make_test_reports 5 | 6 | class Command(NoArgsCommand): 7 | """Generate the test reports from the latest commits.""" 8 | 9 | help = "Generate the test reports from the latest commits." 10 | 11 | def handle_noargs(self, **options): 12 | make_test_reports(force=True) 13 | -------------------------------------------------------------------------------- /djintegration/management/commands/maketestreports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.management.base import NoArgsCommand 3 | from django.conf import settings 4 | from djintegration.commands import make_test_reports 5 | 6 | class Command(NoArgsCommand): 7 | """Generate the test reports from the latest commits.""" 8 | 9 | help = "Generate the test reports from the latest commits." 10 | 11 | def handle_noargs(self, **options): 12 | make_test_reports() 13 | -------------------------------------------------------------------------------- /djintegration/models.py: -------------------------------------------------------------------------------- 1 | """Repositories ``models``.""" 2 | from djintegration.settings import INT_DIR 3 | 4 | from django.db import models 5 | from django.contrib.auth.models import User 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from datetime import datetime 9 | import string 10 | import hashlib 11 | import os 12 | 13 | try: 14 | from djintegration.tasks import MakeTestReportsTask 15 | except: 16 | pass 17 | 18 | REPOS = ( 19 | ('git', 'Git'), 20 | ('svn', 'Subversion'), 21 | ('hg', 'Mercurial'), 22 | ) 23 | 24 | 25 | STATE = ( 26 | ('fail', 'Fail'), 27 | ('pass', 'Pass'), 28 | ('running', 'Running'), 29 | ) 30 | 31 | VIRTUAL_ENV = ( 32 | ('vs', 'Virtualenv with Setuptools'), 33 | ('vd', 'Virtualenv with Distribute >= 1.4.4'), 34 | #('bs', 'Buildout with Setuptools'), 35 | #('bd', 'Buildout with Distribute'), 36 | ('no', 'No virtual environnement'), 37 | ) 38 | 39 | 40 | class Repository(models.Model): 41 | """Represent a repository""" 42 | 43 | name = models.CharField(_('Project Name'), blank=False, max_length=100, unique=True) 44 | url = models.CharField(_('URL'), blank=False, max_length=250) 45 | type = models.CharField(_('Type'), choices=REPOS, max_length=10, default='git') 46 | last_commit = models.CharField(_('Last commit'), max_length=100, 47 | blank=True) 48 | creation_date = models.DateTimeField(_('creation date'), editable=False, 49 | default=datetime.now) 50 | 51 | virtual_env_type = models.CharField(_('Virtual environnement'), 52 | choices=VIRTUAL_ENV, max_length=16, default="vs") 53 | 54 | install_command = models.TextField(_('Install command'), 55 | blank=True, 56 | help_text='Default: "python setup.py install"') 57 | 58 | test_subpath = models.CharField(_('Test subpath'), blank=True, max_length=200, 59 | help_text="Optional subpath to run the test command. Default is '.'") 60 | 61 | test_command = models.TextField(_('Test command'), 62 | blank=True, 63 | help_text='Default: "python setup.py test"') 64 | 65 | state = models.CharField(_('State'), choices=STATE, max_length=10, default='fail') 66 | 67 | emails = models.TextField(_('Notification emails'), 68 | blank=True, 69 | help_text='Default: settings.DJANGO_INTEGRATION_MAILS, comma separated.') 70 | 71 | def fail(self): 72 | latest = self.last_test_report() 73 | if not latest: 74 | return False 75 | return latest.state == 'fail' 76 | 77 | def running(self): 78 | latest = self.last_test_report() 79 | if not latest: 80 | return False 81 | return latest.state == 'running' 82 | 83 | def last_test_report(self): 84 | try: 85 | return TestReport.objects.filter(repository=self).latest('creation_date') 86 | except: 87 | return None 88 | 89 | def get_install_command(self): 90 | return self.install_command or 'python setup.py install' 91 | 92 | def get_test_command(self): 93 | return self.test_command or 'python setup.py test' 94 | 95 | def has_coverage(self): 96 | cov_dir = os.path.join(INT_DIR, self.dirname() + '-cov/') 97 | return os.path.exists(cov_dir) 98 | 99 | def dirname(self): 100 | m = hashlib.md5() 101 | label = "%s%s" % (self.name, self.url) 102 | m.update(label) 103 | return m.hexdigest() 104 | 105 | def coverage_url(self): 106 | return self.dirname() + '-cov/index.html' 107 | 108 | class Meta: 109 | get_latest_by = 'creation_date' 110 | verbose_name = _('Repository') 111 | verbose_name_plural = _('Repositories') 112 | 113 | def __unicode__(self): 114 | return "%s" % (self.name) 115 | 116 | 117 | class TestReport(models.Model): 118 | """Test report""" 119 | repository = models.ForeignKey(Repository) 120 | creation_date = models.DateTimeField(_('creation date'), editable=False, 121 | default=datetime.now) 122 | 123 | commit = models.CharField(_('Commit'), max_length=100, blank=False) 124 | 125 | install = models.TextField(blank=True) 126 | 127 | result = models.TextField(blank=True) 128 | 129 | author = models.CharField(_('Author'), max_length=100, blank=True) 130 | 131 | state = models.CharField(_('State'), choices=STATE, max_length=10) 132 | 133 | def fail(self): 134 | return self.state == 'fail' 135 | 136 | def running(self): 137 | return self.state == 'running' 138 | 139 | class Meta: 140 | verbose_name = _('Test report') 141 | verbose_name_plural = _('Test reports') 142 | 143 | def __unicode__(self): 144 | return "Test on %s for %s: %s" % (self.creation_date.strftime("%c"), 145 | self.repository.name, self.state) 146 | 147 | def result_summary(self): 148 | return "
".join(self.result.split("\n")[-6:]) 149 | 150 | -------------------------------------------------------------------------------- /djintegration/settings.py: -------------------------------------------------------------------------------- 1 | # To keep tests running 2 | from django.conf import settings 3 | 4 | INT_DIR = getattr(settings, 'DJANGO_INTEGRATION_DIRECTORY', '/tmp/dji/') 5 | COV_CANDIDATES = getattr(settings, 'DJANGO_INTEGRATION_COV_CANDIDATES', 6 | ['htmlcov', 'covhtml', 'cov', 'coverage']) 7 | TESTED_APP_DIR = getattr(settings, 'DJANGO_TESTED_APP_DIR', 'tested_app/') 8 | 9 | # 20 minutes max running time for the tests 10 | MAX_RUNNING_TIME = getattr(settings, 'MAX_RUNNING_TIME', 20 * 60) -------------------------------------------------------------------------------- /djintegration/static/djintegration/main.css: -------------------------------------------------------------------------------- 1 | h1{ 2 | font:2.1em Georgia,"Times New Roman",Times,serif; 3 | font-size:2.2em; 4 | margin:0 0 1em 0; 5 | } 6 | h1 span { 7 | color:#777; 8 | } 9 | 10 | h2{ 11 | font:1.5em Georgia,"Times New Roman",Times,serif; 12 | } 13 | h3{ 14 | font:1.2em Georgia,"Times New Roman",Times,serif; 15 | } 16 | h1, h2, h3, a { 17 | color:#bb3010; 18 | } 19 | body { 20 | padding:20px; 21 | color:#222; 22 | font-family:Arial,Helvetica,sans-serif; 23 | font-size:13px; 24 | 25 | } 26 | 27 | 28 | .launch { 29 | -moz-box-shadow:inset 0px 1px 0px 0px #caefab; 30 | -webkit-box-shadow:inset 0px 1px 0px 0px #caefab; 31 | box-shadow:inset 0px 1px 0px 0px #caefab; 32 | background-color:#77d42a; 33 | -moz-border-radius:6px; 34 | -webkit-border-radius:6px; 35 | border-radius:6px; 36 | border:1px solid #268a16; 37 | display:inline-block; 38 | color:#306108; 39 | font-family:arial; 40 | font-size:15px; 41 | font-weight:bold; 42 | padding:6px 24px; 43 | text-decoration:none; 44 | text-shadow:1px 1px 0px #aade7c; 45 | }.launch:hover { 46 | background-color:#5cb811; 47 | }.launch:active { 48 | position:relative; 49 | top:1px; 50 | } 51 | 52 | .running .launch { 53 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; 54 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; 55 | box-shadow:inset 0px 1px 0px 0px #ffffff; 56 | background-color:#ededed; 57 | -moz-border-radius:6px; 58 | -webkit-border-radius:6px; 59 | border-radius:6px; 60 | border:1px solid #dcdcdc; 61 | display:inline-block; 62 | color:#777777; 63 | font-family:arial; 64 | font-size:15px; 65 | font-weight:bold; 66 | padding:6px 24px; 67 | text-decoration:none; 68 | text-shadow:1px 1px 0px #ffffff; 69 | } 70 | .running .launch:hover { 71 | background-color:#ededed 72 | } 73 | .running .launch:active { 74 | position:relative; 75 | top:1px; 76 | } 77 | 78 | 79 | .runningb { 80 | 81 | } 82 | 83 | .repository, .test { 84 | width:45%; 85 | min-width:400px; 86 | margin-right:2%; 87 | margin-bottom:2%; 88 | border:1px #ddd solid; 89 | float:left; 90 | -webkit-box-shadow: 5px 5px 3px #ddd; 91 | -moz-box-shadow: 5px 5px 3px #ddd; 92 | box-shadow: 5px 5px 3px #ddd; 93 | padding:1%; 94 | } 95 | 96 | .report pre { 97 | border:1px #bbb solid; 98 | padding:0.5em; 99 | overflow:hidden; 100 | background-color:#bbf650; 101 | cursor:pointer; 102 | } 103 | 104 | .pass pre { 105 | background-color:#bbf650; 106 | } 107 | 108 | .running pre { 109 | background-color:#ff8c00; 110 | } 111 | 112 | .collapsed pre { 113 | height:2em; 114 | } 115 | 116 | td { padding:0.2em 0.5em; border-bottom:2px #ddd solid; } 117 | table { width:100%; border-collapse:collapse; } 118 | 119 | .fail pre { 120 | background-color:#f64320; 121 | } 122 | 123 | #main { 124 | overflow:hidden 125 | } 126 | 127 | input[type="checkbox"] { 128 | margin:0; 129 | padding:0; 130 | vertical-align:bottom; 131 | } -------------------------------------------------------------------------------- /djintegration/static/djintegration/main.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $("body").on('click', '.report', function() { 3 | $(this).toggleClass('collapsed'); 4 | return false; 5 | }); 6 | 7 | 8 | function updateRepo(button) { 9 | var id = $(button).parents(".repository").attr("data-id"); 10 | $.get("repopart/"+id, function(data) { 11 | $(button).parents(".repository").replaceWith(data); 12 | }); 13 | } 14 | 15 | function checkStatus(id, l) { 16 | $.get("taskstatus/"+id, function(data) { 17 | if(data=="False") { 18 | $(l).text($(l).text()+'.'); 19 | setTimeout(function(){checkStatus(id, l);}, 800); 20 | } else { 21 | $(l).text($(l).text() + " Done, please wait for results."); 22 | $(l).parents(".repository").removeClass("running"); 23 | //$(l).removeClass("runningb"); 24 | // just enough time to see the result 25 | setTimeout(function(){updateRepo(l);}, 2000); 26 | 27 | } 28 | }); 29 | } 30 | 31 | $("body").on("click", ".launch", function(e) { 32 | e.preventDefault(); 33 | var l = this; 34 | var repo = $(this).parents(".repository") 35 | if($(this).hasClass("runningb")) { 36 | return false; 37 | } 38 | $(this).addClass("runningb"); 39 | 40 | repo.addClass("running"); 41 | var force = repo.find(".force")[0].checked; 42 | $.get(l.href+"?force="+force, function(data) { 43 | $(l).text("Running "); 44 | checkStatus(data, l); 45 | }); 46 | 47 | return false; 48 | }); 49 | }); -------------------------------------------------------------------------------- /djintegration/tasks.py: -------------------------------------------------------------------------------- 1 | import os, datetime 2 | from celery.task import Task 3 | from celery.registry import tasks 4 | from djintegration.models import TestReport 5 | from djintegration.commands import make_test_reports 6 | from djintegration.backends import get_backend 7 | 8 | fname = '/tmp/making_reports' 9 | 10 | 11 | 12 | class MakeTestReportsTask(Task): 13 | 14 | def run(self, **kwargs): 15 | numreports = TestReport.objects.count() 16 | make_test_reports() 17 | 18 | class ForceTestReportsTask(Task): 19 | 20 | def run(self, **kwargs): 21 | numreports = TestReport.objects.count() 22 | make_test_reports(force=True) 23 | 24 | class MakeTestReportTask(Task): 25 | 26 | def run(self, repository, force, **kwargs): 27 | 28 | backend = get_backend(repository) 29 | backend.make_report(force) 30 | -------------------------------------------------------------------------------- /djintegration/templates/djintegration/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load djintegration_tags %} 3 | 4 | 5 | {% block title %}Latest test reports from djintegration{% endblock %} 6 | 7 | 8 | 9 | 10 |
11 | {% block body %} 12 | {% endblock %} 13 |
14 | 15 | -------------------------------------------------------------------------------- /djintegration/templates/djintegration/error_email.html: -------------------------------------------------------------------------------- 1 | {% load djintegration_tags %} 2 | Woops, 3 | 4 | Tests on repository {{ test.repository.url }} 5 | run {{ test.creation_date|date_diff }} 6 | on the commit {{ test.commit }} didn't passed. 7 | Latest commiter was {{ test.author|safe }}. 8 | 9 | Tests results output 10 | ({{ test.repository.get_test_command|safe }}) 11 | ========================= 12 | 13 | {{ test.result|safe }} 14 | 15 | 16 | Tests installation output 17 | ========================== 18 | 19 | {{ test.install|safe }} 20 | -------------------------------------------------------------------------------- /djintegration/templates/djintegration/latest_reports.html: -------------------------------------------------------------------------------- 1 | {% extends "djintegration/base.html" %} 2 | {% load djintegration_tags %} 3 | {% block body %} 4 |

DCI | Fresh test reports | admin

5 | 6 | {% for repo in repos %} 7 | {% include "djintegration/repository_partial.html" %} 8 | {% endfor %} 9 | 10 | 11 | 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /djintegration/templates/djintegration/repository.html: -------------------------------------------------------------------------------- 1 | {% extends "djintegration/base.html" %} 2 | {% load djintegration_tags %} 3 | {% block body %} 4 | « Back to fresh reports. 5 |

DCI | Latest tests for {{ repo.name }} | Repo administration

6 | 7 | {% for test in tests %} 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
Url{{ repo.url }}
Commit{{ test.commit }}
Last run{{ test.creation_date|date_diff }}
Author{{ test.author }}
Result{{ test.result_summary|safe }}
17 | 18 | {% if test.install %} 19 |

Install output

20 | 23 | {% endif %} 24 | 25 |

Test output

26 | 29 |
30 | {% endfor %} 31 | 32 | 33 | 41 | 42 | 43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /djintegration/templates/djintegration/repository_partial.html: -------------------------------------------------------------------------------- 1 | {% load djintegration_tags %} 2 |
3 |

{{ repo.name }}

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% if repo.has_coverage %} 12 | 13 | {% endif %} 14 |
Url{{ repo.url }}
Commit{{ repo.last_test_report.commit }}
Last run{{ repo.last_test_report.creation_date|date_diff }}
Author{{ repo.last_test_report.author }}
Result{{ repo.last_test_report.result_summary|safe }}
CoverageShow the report
15 | 16 | {% if repo.fail %} 17 | {% if repo.last_test_report.install %} 18 |

Install output

19 | 22 | {% endif %} 23 | {% endif %} 24 |

Test output

25 | 28 | 29 | {% if not repo.running %} 30 |

31 | Run the tests 32 |

33 | 34 | 35 | {% endif %} 36 |
-------------------------------------------------------------------------------- /djintegration/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-continuous-integration/7ba39cac31592448bc516df023e49517aa6ad400/djintegration/templatetags/__init__.py -------------------------------------------------------------------------------- /djintegration/templatetags/djintegration_tags.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.utils.translation import ungettext, ugettext as _ 3 | from django import template 4 | register = template.Library() 5 | 6 | @register.filter 7 | def date_diff(d): 8 | if not d: 9 | return '' 10 | 11 | now = datetime.datetime.now() 12 | today = datetime.datetime(now.year, now.month, now.day) 13 | delta = now - d 14 | delta_midnight = today - d 15 | days = delta.days 16 | hours = round(delta.seconds / 3600., 0) 17 | minutes = round(delta.seconds / 60., 0) 18 | chunks = ( 19 | (365.0, lambda n: ungettext('year', 'years', n)), 20 | (30.0, lambda n: ungettext('month', 'months', n)), 21 | (7.0, lambda n: ungettext('week', 'weeks', n)), 22 | (1.0, lambda n: ungettext('day', 'days', n)), 23 | ) 24 | 25 | if days < 0: 26 | return _("just now") 27 | 28 | if days == 0: 29 | if hours == 0: 30 | if minutes > 0: 31 | return ungettext('1 minute ago', \ 32 | '%(minutes)d minutes ago', minutes) % \ 33 | {'minutes': minutes} 34 | else: 35 | return _("just now") 36 | else: 37 | return ungettext('1 hour ago', '%(hours)d hours ago', hours) \ 38 | % {'hours': hours} 39 | 40 | if delta_midnight.days == 0: 41 | return _("yesterday at %s") % d.strftime("%H:%M") 42 | 43 | count = 0 44 | for i, (chunk, name) in enumerate(chunks): 45 | if days >= chunk: 46 | count = round((delta_midnight.days + 1)/chunk, 0) 47 | break 48 | 49 | return _('%(number)d %(type)s ago') % \ 50 | {'number': count, 'type': name(count)} 51 | -------------------------------------------------------------------------------- /djintegration/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from djintegration.tests.source_control import SourceControlTestCase -------------------------------------------------------------------------------- /djintegration/tests/gitlog.txt: -------------------------------------------------------------------------------- 1 | commit 0873bdde7f216304800fe1a22325cb60ee492f54 2 | Author: Batiste Bieler 3 | Date: Thu Feb 18 10:15:07 2010 +0100 4 | 5 | Document the database change 6 | -------------------------------------------------------------------------------- /djintegration/tests/hglog.txt: -------------------------------------------------------------------------------- 1 | changeset: 164:7bc186caa7dc 2 | tag: tip 3 | user: Tata Toto 4 | date: Fri Jan 15 22:53:05 2010 +0100 5 | summary: Message summary 6 | -------------------------------------------------------------------------------- /djintegration/tests/source_control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django continuous integration test suite. 3 | """ 4 | from djintegration.backends import GitBackend, SvnBackend, MercurialBackend 5 | 6 | import unittest 7 | import os 8 | TEST_DIR = os.path.dirname(__file__) 9 | 10 | class SourceControlTestCase(unittest.TestCase): 11 | """Django continuous integration test suite. class""" 12 | 13 | def test_backends(self): 14 | 15 | git = GitBackend('fake') 16 | log = open(TEST_DIR+'/gitlog.txt').read() 17 | self.assertEqual(git.get_commit(log), 18 | '0873bdde7f216304800fe1a22325cb60ee492f54') 19 | self.assertEqual(git.get_author(log), 20 | 'Batiste Bieler ') 21 | 22 | svn = SvnBackend('fake') 23 | log = open(TEST_DIR+'/svninfo.txt').read() 24 | self.assertEqual(svn.get_commit(log), '466') 25 | self.assertEqual(svn.get_author(log), 'sehmaschine') 26 | 27 | svn = MercurialBackend('fake') 28 | log = open(TEST_DIR+'/hglog.txt').read() 29 | self.assertEqual(svn.get_commit(log), '164:7bc186caa7dc') 30 | self.assertEqual(svn.get_author(log), 'Tata Toto ') -------------------------------------------------------------------------------- /djintegration/tests/svninfo.txt: -------------------------------------------------------------------------------- 1 | Path: . 2 | URL: http://django-filebrowser.googlecode.com/svn/branches/filebrowser_3 3 | Repository Root: http://django-filebrowser.googlecode.com/svn 4 | Repository UUID: b1eb0509-c838-0410-b15c-e5ce2c1b85ca 5 | Revision: 466 6 | Node Kind: directory 7 | Schedule: normal 8 | Last Changed Author: sehmaschine 9 | Last Changed Rev: 466 10 | Last Changed Date: 2009-10-29 17:15:59 +0100 (Thu, 29 Oct 2009) 11 | -------------------------------------------------------------------------------- /djintegration/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from djintegration.settings import INT_DIR 3 | from django.views.static import serve 4 | 5 | urlpatterns = patterns('djintegration.views', 6 | url(r'^$', 'latest_reports', name="latest-reports"), 7 | url(r'^repo/(?P[0-9]+)$', 'repository', name="repository"), 8 | url(r'^repopart/(?P[0-9]+)$', 'repository_partial', name="repository-partial"), 9 | url(r'^code/(?P.*)$', serve, 10 | {'document_root': INT_DIR, 'show_indexes': True}, name='serve-integration-dir'), 11 | url(r'^makeall$', 'make_reports', name="make-reports"), 12 | url(r'^forceall$', 'force_reports', name="force-reports"), 13 | url(r'^make/(?P[0-9]+)$', 'make_report', name="make-report"), 14 | url(r'^taskstatus/(?P[0-9a-zA-Z\-]+)$', 'task_status', name="task-status"), 15 | ) 16 | -------------------------------------------------------------------------------- /djintegration/views.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from djintegration.models import Repository, TestReport 3 | from djintegration.tasks import MakeTestReportsTask 4 | from djintegration.tasks import ForceTestReportsTask, MakeTestReportTask 5 | from django.shortcuts import render_to_response, redirect 6 | from django.template import RequestContext 7 | from django import http 8 | from celery.result import AsyncResult 9 | 10 | def latest_reports(request): 11 | 12 | repos = list(Repository.objects.filter(state='fail')) + \ 13 | list(Repository.objects.filter(state='pass')) 14 | 15 | return render_to_response('djintegration/latest_reports.html', 16 | RequestContext(request, locals())) 17 | 18 | def repository(request, repo_id): 19 | 20 | repo = Repository.objects.get(pk=int(repo_id)) 21 | tests = TestReport.objects.filter(repository=repo).order_by('-creation_date')[0:30] 22 | 23 | return render_to_response('djintegration/repository.html', 24 | RequestContext(request, locals())) 25 | 26 | def repository_partial(request, repo_id): 27 | repo = Repository.objects.get(pk=int(repo_id)) 28 | return render_to_response('djintegration/repository_partial.html', 29 | RequestContext(request, {"repo":repo})) 30 | 31 | def make_reports(request): 32 | if not request.user.is_staff: 33 | http.HttpResponse("Not allowed") 34 | 35 | MakeTestReportsTask.delay() 36 | response = latest_reports(request) 37 | return redirect('/') 38 | 39 | def force_reports(request): 40 | if not request.user.is_staff: 41 | http.HttpResponse("Not allowed") 42 | 43 | ForceTestReportsTask.delay() 44 | response = latest_reports(request) 45 | return redirect('/') 46 | 47 | def make_report(request, repo_id): 48 | 49 | force = request.GET.get("force") == "true" 50 | 51 | if not request.user.is_staff: 52 | http.HttpResponse("Not allowed") 53 | 54 | repo = Repository.objects.get(pk=int(repo_id)) 55 | task = MakeTestReportTask() 56 | result = task.delay(repo, force) 57 | 58 | return http.HttpResponse(result.id) 59 | 60 | def task_status(request, task_id): 61 | 62 | res = AsyncResult(task_id) 63 | return http.HttpResponse(str(res.ready())) 64 | -------------------------------------------------------------------------------- /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 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " pickle to make pickle files" 20 | @echo " json to make JSON files" 21 | @echo " htmlhelp to make HTML files and a HTML help project" 22 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 23 | @echo " changes to make an overview over all changed/added/deprecated items" 24 | @echo " linkcheck to check all external links for integrity" 25 | 26 | clean: 27 | -rm -rf .build/* 28 | 29 | html: 30 | mkdir -p .build/html .build/doctrees 31 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html 32 | @echo 33 | @echo "Build finished. The HTML pages are in .build/html." 34 | 35 | pickle: 36 | mkdir -p .build/pickle .build/doctrees 37 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle 38 | @echo 39 | @echo "Build finished; now you can process the pickle files." 40 | 41 | web: pickle 42 | 43 | json: 44 | mkdir -p .build/json .build/doctrees 45 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json 46 | @echo 47 | @echo "Build finished; now you can process the JSON files." 48 | 49 | htmlhelp: 50 | mkdir -p .build/htmlhelp .build/doctrees 51 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp 52 | @echo 53 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 54 | ".hhp project file in .build/htmlhelp." 55 | 56 | latex: 57 | mkdir -p .build/latex .build/doctrees 58 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex 59 | @echo 60 | @echo "Build finished; the LaTeX files are in .build/latex." 61 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 62 | "run these through (pdf)latex." 63 | 64 | changes: 65 | mkdir -p .build/changes .build/doctrees 66 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes 67 | @echo 68 | @echo "The overview file is in .build/changes." 69 | 70 | linkcheck: 71 | mkdir -p .build/linkcheck .build/doctrees 72 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck 73 | @echo 74 | @echo "Link check complete; look for any errors in the above output " \ 75 | "or in .build/linkcheck/output.txt." 76 | -------------------------------------------------------------------------------- /docs/_ext/applyxrefs.py: -------------------------------------------------------------------------------- 1 | """Adds xref targets to the top of files.""" 2 | 3 | import sys 4 | import os 5 | 6 | testing = False 7 | 8 | DONT_TOUCH = ( 9 | './index.txt', 10 | ) 11 | 12 | 13 | def target_name(fn): 14 | if fn.endswith('.txt'): 15 | fn = fn[:-4] 16 | return '_' + fn.lstrip('./').replace('/', '-') 17 | 18 | 19 | def process_file(fn, lines): 20 | lines.insert(0, '\n') 21 | lines.insert(0, '.. %s:\n' % target_name(fn)) 22 | try: 23 | f = open(fn, 'w') 24 | except IOError: 25 | print("Can't open %s for writing. Not touching it." % fn) 26 | return 27 | try: 28 | f.writelines(lines) 29 | except IOError: 30 | print("Can't write to %s. Not touching it." % fn) 31 | finally: 32 | f.close() 33 | 34 | 35 | def has_target(fn): 36 | try: 37 | f = open(fn, 'r') 38 | except IOError: 39 | print("Can't open %s. Not touching it." % fn) 40 | return (True, None) 41 | readok = True 42 | try: 43 | lines = f.readlines() 44 | except IOError: 45 | print("Can't read %s. Not touching it." % fn) 46 | readok = False 47 | finally: 48 | f.close() 49 | if not readok: 50 | return (True, None) 51 | 52 | #print fn, len(lines) 53 | if len(lines) < 1: 54 | print("Not touching empty file %s." % fn) 55 | return (True, None) 56 | if lines[0].startswith('.. _'): 57 | return (True, None) 58 | return (False, lines) 59 | 60 | 61 | def main(argv=None): 62 | if argv is None: 63 | argv = sys.argv 64 | 65 | if len(argv) == 1: 66 | argv.extend('.') 67 | 68 | files = [] 69 | for root in argv[1:]: 70 | for (dirpath, dirnames, filenames) in os.walk(root): 71 | files.extend([(dirpath, f) for f in filenames]) 72 | files.sort() 73 | files = [os.path.join(p, fn) for p, fn in files if fn.endswith('.txt')] 74 | #print files 75 | 76 | for fn in files: 77 | if fn in DONT_TOUCH: 78 | print("Skipping blacklisted file %s." % fn) 79 | continue 80 | 81 | target_found, lines = has_target(fn) 82 | if not target_found: 83 | if testing: 84 | print '%s: %s' % (fn, lines[0]), 85 | else: 86 | print "Adding xref to %s" % fn 87 | process_file(fn, lines) 88 | else: 89 | print "Skipping %s: already has a xref" % fn 90 | 91 | if __name__ == '__main__': 92 | sys.exit(main()) 93 | -------------------------------------------------------------------------------- /docs/_ext/literals_to_xrefs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runs through a reST file looking for old-style literals, and helps replace them 3 | with new-style references. 4 | """ 5 | 6 | import re 7 | import sys 8 | import shelve 9 | 10 | refre = re.compile(r'``([^`\s]+?)``') 11 | 12 | ROLES = ( 13 | 'attr', 14 | 'class', 15 | "djadmin", 16 | 'data', 17 | 'exc', 18 | 'file', 19 | 'func', 20 | 'lookup', 21 | 'meth', 22 | 'mod', 23 | "djadminopt", 24 | "ref", 25 | "setting", 26 | "term", 27 | "tfilter", 28 | "ttag", 29 | 30 | # special 31 | "skip", 32 | ) 33 | 34 | ALWAYS_SKIP = [ 35 | "NULL", 36 | "True", 37 | "False", 38 | ] 39 | 40 | 41 | def fixliterals(fname): 42 | data = open(fname).read() 43 | 44 | last = 0 45 | new = [] 46 | storage = shelve.open("/tmp/literals_to_xref.shelve") 47 | lastvalues = storage.get("lastvalues", {}) 48 | 49 | for m in refre.finditer(data): 50 | 51 | new.append(data[last:m.start()]) 52 | last = m.end() 53 | 54 | line_start = data.rfind("\n", 0, m.start()) 55 | line_end = data.find("\n", m.end()) 56 | prev_start = data.rfind("\n", 0, line_start) 57 | next_end = data.find("\n", line_end + 1) 58 | 59 | # Skip always-skip stuff 60 | if m.group(1) in ALWAYS_SKIP: 61 | new.append(m.group(0)) 62 | continue 63 | 64 | # skip when the next line is a title 65 | next_line = data[m.end():next_end].strip() 66 | if next_line[0] in "!-/:-@[-`{-~" and \ 67 | all(c == next_line[0] for c in next_line): 68 | new.append(m.group(0)) 69 | continue 70 | 71 | sys.stdout.write("\n"+"-"*80+"\n") 72 | sys.stdout.write(data[prev_start+1:m.start()]) 73 | sys.stdout.write(colorize(m.group(0), fg="red")) 74 | sys.stdout.write(data[m.end():next_end]) 75 | sys.stdout.write("\n\n") 76 | 77 | replace_type = None 78 | while replace_type is None: 79 | replace_type = raw_input( 80 | colorize("Replace role: ", fg="yellow")).strip().lower() 81 | if replace_type and replace_type not in ROLES: 82 | replace_type = None 83 | 84 | if replace_type == "": 85 | new.append(m.group(0)) 86 | continue 87 | 88 | if replace_type == "skip": 89 | new.append(m.group(0)) 90 | ALWAYS_SKIP.append(m.group(1)) 91 | continue 92 | 93 | default = lastvalues.get(m.group(1), m.group(1)) 94 | if default.endswith("()") and \ 95 | replace_type in ("class", "func", "meth"): 96 | default = default[:-2] 97 | replace_value = raw_input( 98 | colorize("Text [", fg="yellow") + default + \ 99 | colorize("]: ", fg="yellow")).strip() 100 | if not replace_value: 101 | replace_value = default 102 | new.append(":%s:`%s`" % (replace_type, replace_value)) 103 | lastvalues[m.group(1)] = replace_value 104 | 105 | new.append(data[last:]) 106 | open(fname, "w").write("".join(new)) 107 | 108 | storage["lastvalues"] = lastvalues 109 | storage.close() 110 | 111 | # 112 | # The following is taken from django.utils.termcolors and is copied here to 113 | # avoid the dependancy. 114 | # 115 | 116 | 117 | def colorize(text='', opts=(), **kwargs): 118 | """ 119 | Returns your text, enclosed in ANSI graphics codes. 120 | 121 | Depends on the keyword arguments 'fg' and 'bg', and the contents of 122 | the opts tuple/list. 123 | 124 | Returns the RESET code if no parameters are given. 125 | 126 | Valid colors: 127 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' 128 | 129 | Valid options: 130 | 'bold' 131 | 'underscore' 132 | 'blink' 133 | 'reverse' 134 | 'conceal' 135 | 'noreset' - string will not be auto-terminated with the RESET code 136 | 137 | Examples: 138 | colorize('hello', fg='red', bg='blue', opts=('blink',)) 139 | colorize() 140 | colorize('goodbye', opts=('underscore',)) 141 | print colorize('first line', fg='red', opts=('noreset',)) 142 | print 'this should be red too' 143 | print colorize('and so should this') 144 | print 'this should not be red' 145 | """ 146 | color_names = ('black', 'red', 'green', 'yellow', 147 | 'blue', 'magenta', 'cyan', 'white') 148 | foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) 149 | background = dict([(color_names[x], '4%s' % x) for x in range(8)]) 150 | 151 | RESET = '0' 152 | opt_dict = {'bold': '1', 153 | 'underscore': '4', 154 | 'blink': '5', 155 | 'reverse': '7', 156 | 'conceal': '8'} 157 | 158 | text = str(text) 159 | code_list = [] 160 | if text == '' and len(opts) == 1 and opts[0] == 'reset': 161 | return '\x1b[%sm' % RESET 162 | for k, v in kwargs.iteritems(): 163 | if k == 'fg': 164 | code_list.append(foreground[v]) 165 | elif k == 'bg': 166 | code_list.append(background[v]) 167 | for o in opts: 168 | if o in opt_dict: 169 | code_list.append(opt_dict[o]) 170 | if 'noreset' not in opts: 171 | text = text + '\x1b[%sm' % RESET 172 | return ('\x1b[%sm' % ';'.join(code_list)) + text 173 | 174 | if __name__ == '__main__': 175 | try: 176 | fixliterals(sys.argv[1]) 177 | except (KeyboardInterrupt, SystemExit): 178 | print 179 | -------------------------------------------------------------------------------- /docs/_theme/nature/static/nature.css_t: -------------------------------------------------------------------------------- 1 | /** 2 | * Sphinx stylesheet -- default theme 3 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | */ 5 | 6 | @import url("basic.css"); 7 | 8 | /* -- page layout ----------------------------------------------------------- */ 9 | 10 | body { 11 | font-family: Arial, sans-serif; 12 | font-size: 100%; 13 | background-color: #111; 14 | color: #555; 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | hr{ 20 | border: 1px solid #B1B4B6; 21 | } 22 | 23 | div.document { 24 | background-color: #eee; 25 | } 26 | 27 | div.body { 28 | background-color: #ffffff; 29 | color: #3E4349; 30 | padding: 0 30px 30px 30px; 31 | font-size: 0.8em; 32 | } 33 | 34 | div.footer { 35 | color: #555; 36 | width: 100%; 37 | padding: 13px 0; 38 | text-align: center; 39 | font-size: 75%; 40 | } 41 | 42 | div.footer a { 43 | color: #444; 44 | text-decoration: underline; 45 | } 46 | 47 | div.related { 48 | background-color: #6BA81E; 49 | line-height: 32px; 50 | color: #fff; 51 | text-shadow: 0px 1px 0 #444; 52 | font-size: 0.80em; 53 | } 54 | 55 | div.related a { 56 | color: #E2F3CC; 57 | } 58 | 59 | div.sphinxsidebar { 60 | font-size: 0.75em; 61 | line-height: 1.5em; 62 | } 63 | 64 | div.sphinxsidebarwrapper{ 65 | padding: 20px 0; 66 | } 67 | 68 | div.sphinxsidebar h3, 69 | div.sphinxsidebar h4 { 70 | font-family: Arial, sans-serif; 71 | color: #222; 72 | font-size: 1.2em; 73 | font-weight: normal; 74 | margin: 0; 75 | padding: 5px 10px; 76 | background-color: #ddd; 77 | text-shadow: 1px 1px 0 white 78 | } 79 | 80 | div.sphinxsidebar h4{ 81 | font-size: 1.1em; 82 | } 83 | 84 | div.sphinxsidebar h3 a { 85 | color: #444; 86 | } 87 | 88 | 89 | div.sphinxsidebar p { 90 | color: #888; 91 | padding: 5px 20px; 92 | } 93 | 94 | div.sphinxsidebar p.topless { 95 | } 96 | 97 | div.sphinxsidebar ul { 98 | margin: 10px 20px; 99 | padding: 0; 100 | color: #000; 101 | } 102 | 103 | div.sphinxsidebar a { 104 | color: #444; 105 | } 106 | 107 | div.sphinxsidebar input { 108 | border: 1px solid #ccc; 109 | font-family: sans-serif; 110 | font-size: 1em; 111 | } 112 | 113 | div.sphinxsidebar input[type=text]{ 114 | margin-left: 20px; 115 | } 116 | 117 | /* -- body styles ----------------------------------------------------------- */ 118 | 119 | a { 120 | color: #005B81; 121 | text-decoration: none; 122 | } 123 | 124 | a:hover { 125 | color: #E32E00; 126 | text-decoration: underline; 127 | } 128 | 129 | div.body h1, 130 | div.body h2, 131 | div.body h3, 132 | div.body h4, 133 | div.body h5, 134 | div.body h6 { 135 | font-family: Arial, sans-serif; 136 | background-color: #BED4EB; 137 | font-weight: normal; 138 | color: #212224; 139 | margin: 30px 0px 10px 0px; 140 | padding: 5px 0 5px 10px; 141 | text-shadow: 0px 1px 0 white 142 | } 143 | 144 | div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } 145 | div.body h2 { font-size: 150%; background-color: #C8D5E3; } 146 | div.body h3 { font-size: 120%; background-color: #D8DEE3; } 147 | div.body h4 { font-size: 110%; background-color: #D8DEE3; } 148 | div.body h5 { font-size: 100%; background-color: #D8DEE3; } 149 | div.body h6 { font-size: 100%; background-color: #D8DEE3; } 150 | 151 | a.headerlink { 152 | color: #c60f0f; 153 | font-size: 0.8em; 154 | padding: 0 4px 0 4px; 155 | text-decoration: none; 156 | } 157 | 158 | a.headerlink:hover { 159 | background-color: #c60f0f; 160 | color: white; 161 | } 162 | 163 | div.body p, div.body dd, div.body li { 164 | text-align: justify; 165 | line-height: 1.5em; 166 | } 167 | 168 | div.admonition p.admonition-title + p { 169 | display: inline; 170 | } 171 | 172 | div.highlight{ 173 | background-color: white; 174 | } 175 | 176 | div.note { 177 | background-color: #eee; 178 | border: 1px solid #ccc; 179 | } 180 | 181 | div.seealso { 182 | background-color: #ffc; 183 | border: 1px solid #ff6; 184 | } 185 | 186 | div.topic { 187 | background-color: #eee; 188 | } 189 | 190 | div.warning { 191 | background-color: #ffe4e4; 192 | border: 1px solid #f66; 193 | } 194 | 195 | p.admonition-title { 196 | display: inline; 197 | } 198 | 199 | p.admonition-title:after { 200 | content: ":"; 201 | } 202 | 203 | pre { 204 | padding: 10px; 205 | background-color: White; 206 | color: #222; 207 | line-height: 1.2em; 208 | border: 1px solid #C6C9CB; 209 | font-size: 1.2em; 210 | margin: 1.5em 0 1.5em 0; 211 | -webkit-box-shadow: 1px 1px 1px #d8d8d8; 212 | -moz-box-shadow: 1px 1px 1px #d8d8d8; 213 | } 214 | 215 | tt { 216 | background-color: #ecf0f3; 217 | color: #222; 218 | padding: 1px 2px; 219 | font-size: 1.2em; 220 | font-family: monospace; 221 | } -------------------------------------------------------------------------------- /docs/_theme/nature/static/pygments.css: -------------------------------------------------------------------------------- 1 | .c { color: #999988; font-style: italic } /* Comment */ 2 | .k { font-weight: bold } /* Keyword */ 3 | .o { font-weight: bold } /* Operator */ 4 | .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 5 | .cp { color: #999999; font-weight: bold } /* Comment.preproc */ 6 | .c1 { color: #999988; font-style: italic } /* Comment.Single */ 7 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 8 | .ge { font-style: italic } /* Generic.Emph */ 9 | .gr { color: #aa0000 } /* Generic.Error */ 10 | .gh { color: #999999 } /* Generic.Heading */ 11 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 12 | .go { color: #111 } /* Generic.Output */ 13 | .gp { color: #555555 } /* Generic.Prompt */ 14 | .gs { font-weight: bold } /* Generic.Strong */ 15 | .gu { color: #aaaaaa } /* Generic.Subheading */ 16 | .gt { color: #aa0000 } /* Generic.Traceback */ 17 | .kc { font-weight: bold } /* Keyword.Constant */ 18 | .kd { font-weight: bold } /* Keyword.Declaration */ 19 | .kp { font-weight: bold } /* Keyword.Pseudo */ 20 | .kr { font-weight: bold } /* Keyword.Reserved */ 21 | .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 22 | .m { color: #009999 } /* Literal.Number */ 23 | .s { color: #bb8844 } /* Literal.String */ 24 | .na { color: #008080 } /* Name.Attribute */ 25 | .nb { color: #999999 } /* Name.Builtin */ 26 | .nc { color: #445588; font-weight: bold } /* Name.Class */ 27 | .no { color: #ff99ff } /* Name.Constant */ 28 | .ni { color: #800080 } /* Name.Entity */ 29 | .ne { color: #990000; font-weight: bold } /* Name.Exception */ 30 | .nf { color: #990000; font-weight: bold } /* Name.Function */ 31 | .nn { color: #555555 } /* Name.Namespace */ 32 | .nt { color: #000080 } /* Name.Tag */ 33 | .nv { color: purple } /* Name.Variable */ 34 | .ow { font-weight: bold } /* Operator.Word */ 35 | .mf { color: #009999 } /* Literal.Number.Float */ 36 | .mh { color: #009999 } /* Literal.Number.Hex */ 37 | .mi { color: #009999 } /* Literal.Number.Integer */ 38 | .mo { color: #009999 } /* Literal.Number.Oct */ 39 | .sb { color: #bb8844 } /* Literal.String.Backtick */ 40 | .sc { color: #bb8844 } /* Literal.String.Char */ 41 | .sd { color: #bb8844 } /* Literal.String.Doc */ 42 | .s2 { color: #bb8844 } /* Literal.String.Double */ 43 | .se { color: #bb8844 } /* Literal.String.Escape */ 44 | .sh { color: #bb8844 } /* Literal.String.Heredoc */ 45 | .si { color: #bb8844 } /* Literal.String.Interpol */ 46 | .sx { color: #bb8844 } /* Literal.String.Other */ 47 | .sr { color: #808000 } /* Literal.String.Regex */ 48 | .s1 { color: #bb8844 } /* Literal.String.Single */ 49 | .ss { color: #bb8844 } /* Literal.String.Symbol */ 50 | .bp { color: #999999 } /* Name.Builtin.Pseudo */ 51 | .vc { color: #ff99ff } /* Name.Variable.Class */ 52 | .vg { color: #ff99ff } /* Name.Variable.Global */ 53 | .vi { color: #ff99ff } /* Name.Variable.Instance */ 54 | .il { color: #009999 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_theme/nature/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = nature.css 4 | pygments_style = tango 5 | 6 | [options] 7 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ../Changelog -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | import sys 4 | import os 5 | 6 | # If your extensions are in another directory, add it here. If the directory 7 | # is relative to the documentation root, use os.path.abspath to make it 8 | # absolute, like shown here. 9 | sys.path.insert(0, "../") 10 | import djintegration 11 | 12 | from django.conf import settings 13 | if not settings.configured: 14 | settings.configure() 15 | 16 | # General configuration 17 | # --------------------- 18 | 19 | # Add any Sphinx extension module names here, as strings. 20 | # They can be extensions 21 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 22 | extensions = ['sphinx.ext.autodoc'] 23 | 24 | # Add any paths that contain templates here, relative to this directory. 25 | templates_path = ['.templates'] 26 | 27 | # The suffix of source filenames. 28 | source_suffix = '.rst' 29 | 30 | # The encoding of source files. 31 | #source_encoding = 'utf-8' 32 | 33 | # The master toctree document. 34 | master_doc = 'index' 35 | 36 | # General information about the project. 37 | project = u'djintegration' 38 | copyright = u'2009, Opera Softare (WebTeam)' 39 | 40 | # The version info for the project you're documenting, acts as replacement for 41 | # |version| and |release|, also used in various other places throughout the 42 | # built documents. 43 | # 44 | # The short X.Y version. 45 | version = ".".join(map(str, djintegration.VERSION[0:2])) 46 | # The full version, including alpha/beta/rc tags. 47 | release = djintegration.__version__ 48 | 49 | # The language for content autogenerated by Sphinx. Refer to documentation 50 | # for a list of supported languages. 51 | #language = None 52 | 53 | # There are two options for replacing |today|: either, you set today to some 54 | # non-false value, then it is used: 55 | #today = '' 56 | # Else, today_fmt is used as the format for a strftime call. 57 | #today_fmt = '%B %d, %Y' 58 | 59 | # List of documents that shouldn't be included in the build. 60 | #unused_docs = [] 61 | 62 | # List of directories, relative to source directory, that shouldn't be searched 63 | # for source files. 64 | exclude_trees = ['.build'] 65 | 66 | # The reST default role (used for this markup: `text`) to use for all 67 | # documents. 68 | #default_role = None 69 | 70 | # If true, '()' will be appended to :func: etc. cross-reference text. 71 | add_function_parentheses = True 72 | 73 | # If true, the current module name will be prepended to all description 74 | # unit titles (such as .. function::). 75 | #add_module_names = True 76 | 77 | # If true, sectionauthor and moduleauthor directives will be shown in the 78 | # output. They are ignored by default. 79 | #show_authors = False 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'trac' 83 | 84 | #html_translator_class = "djangodocs.DjangoHTMLTranslator" 85 | 86 | 87 | # Options for HTML output 88 | # ----------------------- 89 | 90 | # The style sheet to use for HTML and HTML Help pages. A file of that name 91 | # must exist either in Sphinx' static/ path, or in one of the custom paths 92 | # given in html_static_path. 93 | #html_style = 'agogo.css' 94 | 95 | # The name for this set of Sphinx documents. If None, it defaults to 96 | # " v documentation". 97 | #html_title = None 98 | 99 | # A shorter title for the navigation bar. Default is the same as html_title. 100 | #html_short_title = None 101 | 102 | # The name of an image file (relative to this directory) to place at the top 103 | # of the sidebar. 104 | #html_logo = None 105 | 106 | # The name of an image file (within the static path) to use as favicon of the 107 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 108 | # pixels large. 109 | #html_favicon = None 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | html_static_path = ['.static'] 115 | 116 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 117 | # using the given strftime format. 118 | #html_last_updated_fmt = '%b %d, %Y' 119 | 120 | # If true, SmartyPants will be used to convert quotes and dashes to 121 | # typographically correct entities. 122 | html_use_smartypants = True 123 | 124 | # Custom sidebar templates, maps document names to template names. 125 | #html_sidebars = {} 126 | 127 | # Additional templates that should be rendered to pages, maps page names to 128 | # template names. 129 | #html_additional_pages = {} 130 | 131 | # If false, no module index is generated. 132 | html_use_modindex = True 133 | 134 | # If false, no index is generated. 135 | html_use_index = True 136 | 137 | # If true, the index is split into individual pages for each letter. 138 | #html_split_index = False 139 | 140 | # If true, the reST sources are included in the HTML build as _sources/. 141 | #html_copy_source = True 142 | 143 | # If true, an OpenSearch description file will be output, and all pages will 144 | # contain a tag referring to it. The value of this option must be the 145 | # base URL from which the finished HTML is served. 146 | #html_use_opensearch = '' 147 | 148 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 149 | #html_file_suffix = '' 150 | 151 | # Output file base name for HTML help builder. 152 | htmlhelp_basename = 'djintegrationdoc' 153 | 154 | 155 | # Options for LaTeX output 156 | # ------------------------ 157 | 158 | # The paper size ('letter' or 'a4'). 159 | #latex_paper_size = 'letter' 160 | 161 | # The font size ('10pt', '11pt' or '12pt'). 162 | #latex_font_size = '10pt' 163 | 164 | # Grouping the document tree into LaTeX files. List of tuples 165 | # (source start file, target name, title, author, document class 166 | # [howto/manual]). 167 | latex_documents = [ 168 | ('index', 'djintegration.tex', ur'djintegration Documentation', 169 | ur'Ask Solem', 'manual'), 170 | ] 171 | 172 | # The name of an image file (relative to this directory) to place at the top of 173 | # the title page. 174 | #latex_logo = None 175 | 176 | # For "manual" documents, if this is true, then toplevel headings are parts, 177 | # not chapters. 178 | #latex_use_parts = False 179 | 180 | # Additional stuff for the LaTeX preamble. 181 | #latex_preamble = '' 182 | 183 | # Documents to append as an appendix to all manuals. 184 | #latex_appendices = [] 185 | 186 | # If false, no module index is generated. 187 | #latex_use_modindex = True 188 | 189 | html_theme = "nature" 190 | html_theme_path = ["_theme"] 191 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | djintegration Documentation 2 | ================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | 9 | introduction 10 | changelog 11 | 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | 20 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | ../README -------------------------------------------------------------------------------- /docs/shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-continuous-integration/7ba39cac31592448bc516df023e49517aa6ad400/docs/shot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-celery -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity = 1 3 | detailed-errors = 1 4 | with-coverage = 1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import codecs 5 | from setuptools import setup, find_packages 6 | #from distribute import setup, find_packages 7 | import djintegration 8 | 9 | import os 10 | templates_dirs = [] 11 | for directory in os.walk('djintegration/templates'): 12 | templates_dirs.append(directory[0][14:]+'/*.*') 13 | 14 | for directory in os.walk('djintegration/tests'): 15 | templates_dirs.append(directory[0][14:]+'/*.txt') 16 | 17 | print templates_dirs 18 | 19 | setup( 20 | name='djintegration', 21 | version=djintegration.__version__, 22 | description=djintegration.__doc__, 23 | author=djintegration.__author__, 24 | author_email=djintegration.__contact__, 25 | url=djintegration.__homepage__, 26 | platforms=["any"], 27 | packages=find_packages(exclude=['testproj', 'testproj.*']), 28 | package_data={'djintegration': templates_dirs}, 29 | zip_safe=False, 30 | test_suite="nose.collector", 31 | install_requires=( 32 | 'nose', 33 | 'django', 34 | 'virtualenv', 35 | #'coverage', 36 | ), 37 | classifiers=[ 38 | "Development Status :: 4 - Beta", 39 | "Framework :: Django", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Intended Audience :: Developers", 43 | "Topic :: Software Development :: Libraries :: Python Modules", 44 | ], 45 | long_description=codecs.open('README', "r", "utf-8").read(), 46 | ) 47 | -------------------------------------------------------------------------------- /testproj/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batiste/django-continuous-integration/7ba39cac31592448bc516df023e49517aa6ad400/testproj/__init__.py -------------------------------------------------------------------------------- /testproj/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | example_dir = os.path.dirname(__file__) 5 | sys.path.insert(0, os.path.join(example_dir, '..')) 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /testproj/middleware.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.http import HttpResponseForbidden 3 | from django.conf import settings 4 | 5 | class RestrictMiddleware(object): 6 | """ 7 | Reject all requests from IP addresses that are not on the whitelist. 8 | (We allow one exception for the sake of an unfuddle commit callback.) 9 | """ 10 | IP_WHITELIST = [] # override this with IP_WHITELIST in settings 11 | ACCESS_DENIED_MESSAGE = 'Access Denied' 12 | REQUIRED_CALLBACK_STRING = 'repository' # 'repository' is a good one for unfuddle 13 | CONFIRM_CALLBACK_STRINGS = ['my_company_domain.com',] # override this in settings 14 | 15 | def process_request(self, request): 16 | """ 17 | Check the request to see if access should be granted. 18 | """ 19 | ipw = getattr(settings, 'IP_WHITELIST', self.IP_WHITELIST) 20 | if request.META['REMOTE_ADDR'] in ipw: 21 | # all requests from whitelisted IPs are allowed. 22 | return None 23 | 24 | # For now we just report some stuff. 25 | # Later we will add some explicit checks to 26 | # restrict this to unfuddle commit callbacks.. 27 | print request.method 28 | print "|%s|" % (request.path) 29 | for key, value in request.REQUEST.iteritems(): 30 | print key, "::", value 31 | sys.stdout.flush() 32 | if request.method == "POST": 33 | if request.path == "/make": 34 | required_string = getattr(settings, 'REQUIRED_CALLBACK_STRING', self.REQUIRED_CALLBACK_STRING) 35 | callback_strings = getattr(settings, 'CONFIRM_CALLBACK_STRINGS', self.CONFIRM_CALLBACK_STRINGS) 36 | for key, value in request.REQUEST.iteritems(): 37 | if required_string in value: 38 | for callback_string in callback_strings: 39 | if callback_string in value: 40 | return None 41 | 42 | # Unexpected request - deny! 43 | m = getattr(settings, 'RESTRICT_ACCESS_DENIED_MESSAGE', self.ACCESS_DENIED_MESSAGE) 44 | return HttpResponseForbidden(m) 45 | 46 | -------------------------------------------------------------------------------- /testproj/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for test_proj project. 2 | import os 3 | PROJECT_DIR = os.path.dirname(__file__) 4 | 5 | DEBUG = True 6 | TEMPLATE_DEBUG = DEBUG 7 | 8 | ADMINS = ( 9 | ('somebody', 'somebody@gmail.com'), 10 | ) 11 | 12 | DJANGO_INTEGRATION_DIRECTORY = '/tmp/dci' 13 | 14 | MANAGERS = ADMINS 15 | 16 | # settings.py 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', 20 | 'NAME': 'dci.db' 21 | } 22 | } 23 | 24 | # Local time zone for this installation. Choices can be found here: 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # although not all choices may be available on all operating systems. 27 | # If running in a Windows environment this must be set to the same as your 28 | # system time zone. 29 | TIME_ZONE = 'America/Chicago' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'en-us' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | MEDIA_ROOT = STATIC_ROOT = os.path.join(PROJECT_DIR, 'media') 42 | MEDIA_URL = '/media/' 43 | 44 | STATIC_ROOT = os.path.join(PROJECT_DIR, 'static') 45 | STATIC_URL = '/static/' 46 | 47 | # Make this unique, and don't share it with anybody. 48 | SECRET_KEY = 'kwjdckjwdcl0@ku!3&wi4kx4$yqnwctw*cf2kmi(0p=#3n!jl!0kp!o18wn^' 49 | 50 | MIDDLEWARE_CLASSES = ( 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware' 55 | #'testproj.middleware.RestrictMiddleware', 56 | ) 57 | 58 | ROOT_URLCONF = 'testproj.urls' 59 | 60 | DJANGO_INTEGRATION_MAILS = ['batisteb@opera.com'] 61 | 62 | INSTALLED_APPS = ( 63 | 'django.contrib.auth', 64 | 'django.contrib.contenttypes', 65 | 'django.contrib.sessions', 66 | 'django.contrib.sites', 67 | 'django.contrib.admin', 68 | 'django.contrib.staticfiles', 69 | 'djcelery', 70 | 'djkombu', 71 | 'djintegration', 72 | ) 73 | 74 | # commit hook callback settings 75 | # Your commit hook must send a POST request to /make 76 | # Some value in the post REQUEST must include the REQUIRED_CALLBACK_STRING as well 77 | # as one of the CONFRIM_CALLBACK_STRINGS. (The IP address of the callback sender 78 | # does not need to be in the IP_WHITELIST.) 79 | # To change this behaviour, reqork the middleware.py file. 80 | # This stuff was tested with unfuddle.com. 81 | 82 | # IP_WHITELIST = ['127.0.0.1'] 83 | # REQUIRED_CALLBACK_STRING = 'repository' 84 | # CONFIRM_CALLBACK_STRINGS = ['mycompany.com'] 85 | 86 | # celery settings 87 | 88 | import djcelery 89 | djcelery.setup_loader() 90 | 91 | BROKER_BACKEND = 'djkombu.transport.DatabaseTransport' 92 | BROKER_HOST = 'localhost' 93 | BROKER_PORT = 5672 94 | BROKER_USER = 'someuser' 95 | BROKER_PASSWORD = 'somepass' 96 | BROKER_VHOST = '/' 97 | 98 | -------------------------------------------------------------------------------- /testproj/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block branding %}

Clarity V5 Continuous Integration Testing Administration -- see Fresh Results

{% endblock %} 4 | -------------------------------------------------------------------------------- /testproj/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | # Example: 8 | # (r'^test_proj/', include('test_proj.foo.urls')), 9 | 10 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 11 | # to INSTALLED_APPS to enable admin documentation: 12 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 13 | 14 | # Uncomment the next line to enable the admin: 15 | (r'^admin/', include(admin.site.urls)), 16 | (r'^', include('djintegration.urls')), 17 | ) 18 | --------------------------------------------------------------------------------