├── .gitignore ├── AUTHORS ├── LICENSE ├── README ├── devmason_server ├── __init__.py ├── admin.py ├── fields.py ├── fixtures │ └── devmason_server_test_data.json ├── forms.py ├── handlers.py ├── models.py ├── signals.py ├── templates │ ├── base.html │ ├── devmason_server │ │ ├── add_project.html │ │ ├── build_detail.html │ │ ├── project_build_list.html │ │ ├── project_detail.html │ │ ├── project_list.html │ │ ├── project_tag_list.html │ │ └── tag_detail.html │ └── registration │ │ ├── activate.html │ │ ├── activation_complete.html │ │ ├── activation_email.txt │ │ ├── activation_email_subject.txt │ │ ├── base.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change_done.html │ │ ├── password_change_form.html │ │ ├── password_reset_complete.html │ │ ├── password_reset_confirm.html │ │ ├── password_reset_done.html │ │ ├── password_reset_email.html │ │ ├── password_reset_form.html │ │ ├── registration_complete.html │ │ └── registration_form.html ├── templatetags │ ├── __init__.py │ └── capture.py ├── tests │ ├── __init__.py │ └── test_api.py ├── urls.py ├── utils.py └── views.py ├── docs ├── Makefile ├── build-server-rest-api.rst ├── conf.py ├── index.rst ├── install.rst └── usage.rst ├── media ├── images │ ├── buttons.gif │ ├── fail.png │ ├── field_bg.gif │ ├── grade_boxers.gif │ ├── grade_briefs.gif │ ├── grade_cutoffs.gif │ ├── grade_pants.gif │ ├── hasselhoffian-recursion.gif │ ├── icon.png │ ├── intro.gif │ ├── konami.gif │ ├── pass.png │ └── rules │ │ └── horz_ddd.gif ├── javascript │ ├── devmason.js │ └── jquery.js └── stylesheets │ └── devmason.css ├── pip_requirements.txt ├── setup.py └── test_project ├── __init__.py ├── manage.py ├── settings.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | docs/_build 4 | dist/ 5 | *.db 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Eric Holscher 2 | Jacob Kaplan-Moss 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Eric Holscher 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Devmason Server. 2 | 3 | A basic Django implementation of the backend server for Pony Build. 4 | 5 | Currently running at devmason.com 6 | -------------------------------------------------------------------------------- /devmason_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/devmason_server/__init__.py -------------------------------------------------------------------------------- /devmason_server/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from devmason_server.models import Project, Build, BuildStep, BuildRequest, Repository 3 | 4 | class BuildAdmin(admin.ModelAdmin): 5 | model = Build 6 | list_display = ('project', 'success', 'user', 'started') 7 | 8 | 9 | admin.site.register(Project) 10 | admin.site.register(Build, BuildAdmin) 11 | admin.site.register(BuildStep) 12 | admin.site.register(Repository) 13 | admin.site.register(BuildRequest) 14 | -------------------------------------------------------------------------------- /devmason_server/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.core.serializers.json import DjangoJSONEncoder 4 | from django.utils import simplejson 5 | 6 | # JSONField is from Djblets 7 | # (http://code.google.com/p/reviewboard/wiki/Djblets), which is by 8 | # Christian Hammond and David Trowbridge. 9 | class JSONField(models.TextField): 10 | """ 11 | A field for storing JSON-encoded data. The data is accessible as standard 12 | Python data types and is transparently encoded/decoded to/from a JSON 13 | string in the database. 14 | """ 15 | serialize_to_string = True 16 | 17 | def __init__(self, verbose_name=None, name=None, 18 | encoder=DjangoJSONEncoder(), **kwargs): 19 | models.TextField.__init__(self, verbose_name, name, blank=True, 20 | **kwargs) 21 | self.encoder = encoder 22 | 23 | def db_type(self): 24 | return "text" 25 | 26 | def contribute_to_class(self, cls, name): 27 | def get_json(model_instance): 28 | return self.dumps(getattr(model_instance, self.attname, None)) 29 | 30 | def set_json(model_instance, json): 31 | setattr(model_instance, self.attname, self.loads(json)) 32 | 33 | super(JSONField, self).contribute_to_class(cls, name) 34 | 35 | setattr(cls, "get_%s_json" % self.name, get_json) 36 | setattr(cls, "set_%s_json" % self.name, set_json) 37 | 38 | models.signals.post_init.connect(self.post_init, sender=cls) 39 | 40 | def pre_save(self, model_instance, add): 41 | return self.dumps(getattr(model_instance, self.attname, None)) 42 | 43 | def post_init(self, instance=None, **kwargs): 44 | value = self.value_from_object(instance) 45 | 46 | if value: 47 | try: 48 | value = self.loads(value) 49 | except (ValueError, SyntaxError): 50 | #Was getting "''", and breaking 51 | pass 52 | else: 53 | value = {} 54 | 55 | setattr(instance, self.attname, value) 56 | 57 | def get_db_prep_save(self, value): 58 | if not isinstance(value, basestring): 59 | value = self.dumps(value) 60 | 61 | return super(JSONField, self).get_db_prep_save(value) 62 | 63 | def value_to_string(self, obj): 64 | return self.dumps(self.value_from_object(obj)) 65 | 66 | def dumps(self, data): 67 | return self.encoder.encode(data) 68 | 69 | def loads(self, val): 70 | try: 71 | val = simplejson.loads(val, encoding=settings.DEFAULT_CHARSET) 72 | 73 | # XXX We need to investigate why this is happening once we have 74 | # a solid repro case. 75 | if isinstance(val, basestring): 76 | # logging.warning("JSONField decode error. Expected dictionary, " 77 | # "got string for input '%s'" % val) 78 | # For whatever reason, we may have gotten back 79 | val = simplejson.loads(val, encoding=settings.DEFAULT_CHARSET) 80 | except ValueError: 81 | # There's probably embedded unicode markers (like u'foo') in the 82 | # string. We have to eval it. 83 | val = eval(val) 84 | 85 | return val 86 | -------------------------------------------------------------------------------- /devmason_server/fixtures/devmason_server_test_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "devmason_server.project", 4 | "pk": 1, 5 | "fields": { 6 | "name": "pony", 7 | "slug": "pony" 8 | } 9 | }, 10 | { 11 | "model": "devmason_server.build", 12 | "pk": 1, 13 | "fields": { 14 | "project": 1, 15 | "success": true, 16 | "started": "2009-10-19 16:22:00", 17 | "finished": "2009-10-19 16:25:00", 18 | "host": "example.com", 19 | "arch": "linux-i386" 20 | } 21 | }, 22 | { 23 | "model": "devmason_server.buildstep", 24 | "pk": 1, 25 | "fields": { 26 | "build": 1, 27 | "success": true, 28 | "started": "2009-10-19 16:22:00", 29 | "finished": "2009-10-19 16:21:00", 30 | "name": "checkout", 31 | "output": "OK" 32 | } 33 | }, 34 | { 35 | "model": "devmason_server.buildstep", 36 | "pk": 2, 37 | "fields": { 38 | "build": 1, 39 | "success": true, 40 | "started": "2009-10-19 16:21:30", 41 | "finished": "2009-10-19 16:25:00", 42 | "name": "test", 43 | "output": "OK" 44 | } 45 | } 46 | ] -------------------------------------------------------------------------------- /devmason_server/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class ProjectForm(forms.Form): 4 | """ 5 | Form to accept a new project submission. 6 | """ 7 | name = forms.CharField(max_length=100) 8 | source_repo = forms.CharField() 9 | -------------------------------------------------------------------------------- /devmason_server/handlers.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django.core.paginator import Paginator, InvalidPage 3 | from django.core import urlresolvers 4 | from django.http import Http404, HttpResponseForbidden, HttpResponseBadRequest 5 | from django.shortcuts import get_object_or_404, redirect 6 | from django.utils import simplejson 7 | from piston.handler import BaseHandler 8 | from piston.utils import require_mime 9 | from tagging.models import Tag 10 | from .models import Project, Build, BuildStep 11 | from .utils import (link, allow_404, authentication_required, 12 | authentication_optional, format_dt, HttpResponseCreated, 13 | HttpResponseNoContent, mk_datetime) 14 | 15 | class ProjectListHandler(BaseHandler): 16 | allowed_methods = ['GET'] 17 | viewname = 'project_list' 18 | 19 | def read(self, request): 20 | latest_builds = Build.objects.order_by('-pk')[:10] 21 | projects = Project.objects.filter(builds__isnull=False).distinct() 22 | builds = {} 23 | for project in projects: 24 | try: 25 | builds[project.name] = list(project.builds.order_by('-pk'))[0] 26 | except: 27 | pass 28 | return { 29 | 'projects': projects, 30 | 'latest_builds': latest_builds, 31 | 'builds': builds, 32 | 'links': [link('self', ProjectListHandler)] 33 | } 34 | 35 | class ProjectHandler(BaseHandler): 36 | allowed_methods = ['GET', 'PUT', 'DELETE'] 37 | model = Project 38 | viewname = 'project_detail' 39 | fields = ('name', 'owner', 'links') 40 | 41 | @allow_404 42 | def read(self, request, slug): 43 | return get_object_or_404(Project, slug=slug) 44 | 45 | @require_mime('json') 46 | @authentication_required 47 | def update(self, request, slug): 48 | # Check the one required field in the PUT data -- a name 49 | try: 50 | project_name = request.data['name'] 51 | except (TypeError, KeyError): 52 | return HttpResponseBadRequest() 53 | 54 | # If there's an existing project, the updating user has to match the 55 | # user who created the project 56 | try: 57 | project = Project.objects.get(slug=slug) 58 | except Project.DoesNotExist: 59 | # The project doesn't exist, so save the user if it's a 60 | # new one, create the project, then return 201 created 61 | if request.user.is_new_user: 62 | request.user.save() 63 | project = Project.objects.create(name=project_name, slug=slug, owner=request.user) 64 | return HttpResponseCreated(urlresolvers.reverse(self.viewname, args=[slug])) 65 | 66 | # Okay, so if we fall through to here then we're trying to update an 67 | # existing project. This means checking that the user's allowed to 68 | # do so before updating it. 69 | if request.user.is_new_user or project.owner != request.user: 70 | return HttpResponseForbidden() 71 | 72 | # Hey, look, we get to update this project. 73 | project.name = project_name 74 | project.save() 75 | 76 | # PUT returns the newly updated representation 77 | return self.read(request, slug) 78 | 79 | @allow_404 80 | @authentication_required 81 | def delete(self, request, slug): 82 | project = get_object_or_404(Project, slug=slug) 83 | 84 | # New users don't get to delete packages; neither do non-owners 85 | if request.user.is_new_user or project.owner != request.user: 86 | return HttpResponseForbidden() 87 | 88 | # Otherwise delete it. 89 | project.delete() 90 | return HttpResponseNoContent() 91 | 92 | @classmethod 93 | def owner(cls, project): 94 | if project.owner: 95 | return project.owner.username 96 | else: 97 | return '' 98 | 99 | @classmethod 100 | def links(cls, project): 101 | return [ 102 | link('self', ProjectHandler, project.slug), 103 | link('build-list', ProjectBuildListHandler, project.slug), 104 | link('latest-build', LatestBuildHandler, project.slug), 105 | link('tag-list', ProjectTagListHandler, project.slug), 106 | ] 107 | 108 | class PaginatedBuildHandler(BaseHandler): 109 | """Helper base class to provide paginated builds""" 110 | 111 | def handle_paginated_builds(self, builds, qdict, link_callback, extra={}): 112 | try: 113 | per_page = int(qdict['per_page']) 114 | except (ValueError, KeyError): 115 | per_page = 25 116 | 117 | paginator = Paginator(builds, per_page) 118 | 119 | try: 120 | page = paginator.page(qdict['page']) 121 | except (KeyError, InvalidPage): 122 | page = paginator.page(1) 123 | 124 | if not page.object_list: 125 | raise Http404("No builds") 126 | 127 | link_callback('self', page=page.number, per_page=per_page) 128 | 129 | if page.has_other_pages(): 130 | link_callback('first', page=1, per_page=per_page) 131 | link_callback('last', page=paginator.num_pages, per_page=per_page) 132 | if page.has_previous(): 133 | link_callback('previous', page=page.previous_page_number(), per_page=per_page) 134 | if page.has_next(): 135 | link_callback('next', page=page.next_page_number(), per_page=per_page) 136 | 137 | response = { 138 | 'builds': page.object_list, 139 | 'count': paginator.count, 140 | 'num_pages': paginator.num_pages, 141 | 'page': page.number, 142 | 'paginated': page.has_other_pages(), 143 | 'per_page': per_page, 144 | } 145 | return dict(response, **extra) 146 | 147 | class ProjectBuildListHandler(PaginatedBuildHandler): 148 | allowed_methods = ['GET', 'POST'] 149 | viewname = 'project_build_list' 150 | 151 | @allow_404 152 | def read(self, request, slug): 153 | project = get_object_or_404(Project, slug=slug) 154 | builds = project.builds.all() 155 | 156 | links = [ 157 | link('project', ProjectHandler, project.slug), 158 | link('latest-build', LatestBuildHandler, project.slug), 159 | ] 160 | 161 | def make_link(rel, **kwargs): 162 | # None of the paginated pages should allow anything other than GET 163 | l = link(rel, self, project.slug, **kwargs) 164 | l['allowed_methods'] = ['GET'] 165 | links.append(l) 166 | 167 | response = self.handle_paginated_builds(builds, request.GET, make_link) 168 | response['links'] = links 169 | response['project'] = project 170 | return response 171 | 172 | @allow_404 173 | @require_mime('json') 174 | @authentication_optional 175 | @transaction.commit_on_success 176 | def create(self, request, slug): 177 | project = get_object_or_404(Project, slug=slug) 178 | 179 | # Construct us the dict of "extra" info from the request 180 | data = request.data 181 | extra = request.data['client'].copy() 182 | for k in ('success', 'started', 'finished', 'client', 'results', 'tags'): 183 | extra.pop(k, None) 184 | 185 | # Create the Build object 186 | try: 187 | build = Build.objects.create( 188 | project = project, 189 | success = data['success'], 190 | started = mk_datetime(data.get('started', '')), 191 | finished = mk_datetime(data.get('finished', '')), 192 | host = data['client']['host'], 193 | arch = data['client']['arch'], 194 | user = request.user.is_authenticated() and request.user or None, 195 | 196 | # Because of some weirdness with the way fields are handled in 197 | # __init__, we have to encode extra as JSON manually here. 198 | # TODO: investiage why and fix JSONField 199 | extra_info = simplejson.dumps(extra), 200 | ) 201 | except (KeyError, ValueError), ex: 202 | # We'll get a KeyError from request.data[k] if the given key 203 | # is missing and ValueError from improperly formatted dates. 204 | # Treat either of these as improperly formatted requests 205 | # and return a 400 (Bad Request) 206 | return HttpResponseBadRequest(str(ex)) 207 | 208 | # Tag us a build 209 | if 'tags' in data: 210 | build.tags = ",".join(data['tags']) 211 | 212 | # Create each build step 213 | for result in request.data.get('results', []): 214 | # extra_info logic as above 215 | extra = result.copy() 216 | for k in ('success', 'started', 'finished', 'name', 'output', 'errout'): 217 | extra.pop(k, None) 218 | 219 | # Create the BuildStep, handling errors as above 220 | try: 221 | BuildStep.objects.create( 222 | build = build, 223 | success = result['success'], 224 | started = mk_datetime(getattr(result, 'started', '')), 225 | finished = mk_datetime(getattr(result, 'finished', '')), 226 | name = result['name'], 227 | output = result.get('output', ''), 228 | errout = result.get('errout', ''), 229 | extra_info = simplejson.dumps(extra), 230 | ) 231 | except (KeyError, ValueError), ex: 232 | # We'll get a KeyError from request.data[k] if the given key 233 | # is missing and ValueError from improperly formatted dates. 234 | # Treat either of these as improperly formatted requests 235 | # and return a 400 (Bad Request) 236 | return HttpResponseBadRequest(str(ex)) 237 | 238 | # It worked! Return a 201 created 239 | url = urlresolvers.reverse(BuildHandler.viewname, args=[project.slug, build.pk]) 240 | return HttpResponseCreated(url) 241 | 242 | class BuildHandler(BaseHandler): 243 | allowed_methods = ['GET'] 244 | model = Build 245 | fields = ('success', 'started', 'finished', 'tags', 246 | 'client', 'results', 'links') 247 | viewname = 'build_detail' 248 | 249 | @allow_404 250 | def read(self, request, slug, build_id): 251 | return get_object_or_404(Build, project__slug=slug, pk=build_id) 252 | 253 | @classmethod 254 | def tags(cls, build): 255 | return [t.name for t in build.tags] 256 | 257 | @classmethod 258 | def started(cls, build): 259 | return format_dt(build.started) 260 | 261 | @classmethod 262 | def finished(cls, build): 263 | return format_dt(build.finished) 264 | 265 | @classmethod 266 | def client(cls, build): 267 | details = { 268 | 'host': build.host, 269 | 'user': build.user and build.user.username or '', 270 | 'arch': build.arch, 271 | } 272 | if build.extra_info != '""': 273 | details.update(build.extra_info) 274 | return details 275 | 276 | @classmethod 277 | def results(cls, build): 278 | rv = [] 279 | for step in build.steps.all(): 280 | step_data = { 281 | 'success': step.success, 282 | 'started': format_dt(step.started), 283 | 'finished': format_dt(step.finished), 284 | 'name': step.name, 285 | 'output': step.output, 286 | 'errout': step.errout, 287 | } 288 | step_data.update(step.extra_info) 289 | rv.append(step_data) 290 | return rv 291 | 292 | @classmethod 293 | def links(cls, build): 294 | links = [ 295 | link('self', BuildHandler, build.project.slug, build.pk), 296 | link('project', ProjectHandler, build.project.slug), 297 | ] 298 | for tag in build.tags: 299 | links.append(link('tag', TagHandler, build.project.slug, tag.name)) 300 | return links 301 | 302 | class LatestBuildHandler(BaseHandler): 303 | allowed_methods = ['GET'] 304 | viewname = 'latest_build' 305 | 306 | @allow_404 307 | def read(self, request, slug): 308 | project = get_object_or_404(Project, slug=slug) 309 | build = list(project.builds.all().order_by('-pk'))[0] 310 | return redirect('build_detail', slug, build.pk) 311 | 312 | class ProjectTagListHandler(BaseHandler): 313 | allowed_methods = ['GET'] 314 | viewname = 'project_tag_list' 315 | 316 | @allow_404 317 | def read(self, request, slug): 318 | project = get_object_or_404(Project, slug=slug) 319 | tags = Tag.objects.usage_for_model(Build, filters={'project': project}) 320 | 321 | links = [ 322 | link('self', ProjectTagListHandler, project.slug), 323 | link('project', ProjectHandler, project.slug), 324 | ] 325 | links.extend(link('tag', TagHandler, project.slug, tag.name) for tag in tags) 326 | 327 | return { 328 | 'tags': [tag.name for tag in tags], 329 | 'links': links, 330 | } 331 | 332 | class TagHandler(PaginatedBuildHandler): 333 | allowed_methods = ['GET'] 334 | viewname = 'tag_detail' 335 | model = Tag 336 | 337 | @allow_404 338 | def read(self, request, slug, tags): 339 | project = get_object_or_404(Project, slug=slug) 340 | tag_list = tags.split(';') 341 | builds = Build.tagged.with_all(tags, queryset=project.builds.all()) 342 | 343 | links = [] 344 | def make_link(rel, **kwargs): 345 | links.append(link(rel, self, project.slug, tags, **kwargs)) 346 | 347 | response = self.handle_paginated_builds(builds, request.GET, make_link, {'tags': tag_list}) 348 | response['links'] = links 349 | return response 350 | 351 | class ProjectLatestTaggedBuildHandler(BaseHandler): 352 | allowed_methods = ['GET'] 353 | viewname = 'latest_tagged_build' 354 | 355 | @allow_404 356 | def read(self, request, slug, tags): 357 | project = get_object_or_404(Project, slug=slug) 358 | tag_list = tags.split(';') 359 | builds = Build.tagged.with_all(tags, queryset=project.builds.all()) 360 | try: 361 | b = builds[0] 362 | except IndexError: 363 | raise Http404("No builds") 364 | return redirect('build_detail', project.slug, b.pk) 365 | -------------------------------------------------------------------------------- /devmason_server/models.py: -------------------------------------------------------------------------------- 1 | import tagging 2 | from django.db import models 3 | from django.contrib.auth.models import User 4 | from .fields import JSONField 5 | 6 | class Project(models.Model): 7 | name = models.CharField(max_length=200) 8 | slug = models.SlugField(unique=True) 9 | owner = models.ForeignKey(User, related_name='projects', blank=True, null=True) 10 | 11 | class Meta: 12 | ordering = ['name'] 13 | 14 | def __unicode__(self): 15 | return self.name 16 | 17 | @models.permalink 18 | def get_absolute_url(self): 19 | return ('project_detail', [self.slug]) 20 | 21 | class Build(models.Model): 22 | project = models.ForeignKey(Project, related_name='builds') 23 | success = models.BooleanField() 24 | started = models.DateTimeField() 25 | finished = models.DateTimeField() 26 | host = models.CharField(max_length=250) 27 | arch = models.CharField(max_length=250) 28 | user = models.ForeignKey(User, blank=True, null=True, related_name='builds') 29 | extra_info = JSONField() 30 | 31 | class Meta: 32 | ordering = ['-finished'] 33 | 34 | def __unicode__(self): 35 | return u"Build %s for %s" % (self.pk, self.project) 36 | 37 | @models.permalink 38 | def get_absolute_url(self): 39 | return ('build_detail', [self.project.slug, self.pk]) 40 | 41 | tagging.register(Build) 42 | 43 | class BuildStep(models.Model): 44 | build = models.ForeignKey(Build, related_name='steps') 45 | success = models.BooleanField() 46 | started = models.DateTimeField() 47 | finished = models.DateTimeField() 48 | name = models.CharField(max_length=250) 49 | output = models.TextField(blank=True) 50 | errout = models.TextField(blank=True) 51 | extra_info = JSONField() 52 | 53 | class Meta: 54 | ordering = ['build', 'started'] 55 | 56 | def __unicode__(self): 57 | return "%s: %s" % (self.build, self.name) 58 | 59 | 60 | VCS_TYPES = ( 61 | ('none', 'None'), 62 | ('git', 'Git'), 63 | ('svn', 'Subversion'), 64 | ('hg', 'Mercurial'), 65 | ('bzr', 'Bazaar'), 66 | ) 67 | 68 | class Repository(models.Model): 69 | """ 70 | Representation of a version control system repository. 71 | """ 72 | project = models.ForeignKey(Project, related_name='repos') 73 | url = models.CharField(max_length=500, unique=True) 74 | type = models.CharField('Version Control Type', max_length=20, 75 | choices=VCS_TYPES, default=VCS_TYPES[0][0]) 76 | 77 | def __unicode__(self): 78 | return self.url 79 | 80 | class BuildRequest(models.Model): 81 | repository = models.ForeignKey(Repository, related_name='build_requests') 82 | identifier = models.CharField(max_length=200) 83 | requested = models.DateTimeField() 84 | 85 | 86 | def __unicode__(self): 87 | return "Build for %s: %s" % (self.repository.project, self.identifier) 88 | 89 | class Meta: 90 | ordering = ['-requested'] 91 | 92 | #import signals 93 | #Make sure signals get reg'd 94 | -------------------------------------------------------------------------------- /devmason_server/signals.py: -------------------------------------------------------------------------------- 1 | import telnetlib 2 | import sys 3 | 4 | from django.db.models.signals import post_save 5 | from devmason_server.models import Build 6 | 7 | pw = "devmason_roxx" 8 | srv = "irc.freenode.net" 9 | chan = "#devmason" 10 | 11 | def send_irc_msg(msg): 12 | try: 13 | t = telnetlib.Telnet() 14 | t.open('10.177.22.217', port=13337) 15 | t.write('%s %s&%s&%s\n' % (pw, srv, chan, msg)) 16 | except: 17 | pass 18 | 19 | def irc_handler(sender, **kwargs): 20 | build = kwargs['instance'] 21 | send_irc_msg('%s: http://devmason.com%s | Passed: %s' % (build, build.get_absolute_url(), build.success)) 22 | 23 | post_save.connect(irc_handler, sender=Build) 24 | -------------------------------------------------------------------------------- /devmason_server/templates/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | {% load i18n %} 5 | 6 | 7 | 8 | 9 | {% block fulltitle %}{% block title %}{% endblock %} | {% trans "DevMason" %}{% endblock %} 10 | 11 | {% block feeds %}{% endblock %} 12 | {% block extended_meta %}{% endblock %} 13 | 14 | 15 | 16 | {% block extended_scripts %}{% endblock %} 17 | 18 | 19 | {% block extended_styles %}{% endblock %} 20 | 21 | 22 | 23 |
24 | {% block header_wrapper %} 25 | 35 | {% endblock %} 36 | 37 | {% block body_wrapper %} 38 | {% load capture %} 39 |
40 | {% block pre_content %}{% endblock %} 41 | 42 |
43 | {% capture as content_title %}{% block content_title %}{% endblock %}{% endcapture %} 44 | {% if content_title %} 45 |
{{ content_title }}
46 | {% endif %} 47 | 48 | {% block content %}{% endblock %} 49 | 57 | 58 |
59 | 60 | {% capture as aside %}{% block links %}{% endblock %}{% endcapture %} 61 | {% if aside %} 62 |
{{ aside }}
63 | {% endif %} 64 |
65 | {% endblock %} 66 | 67 | {% block footer_wrapper %} 68 | 76 | 80 | 85 | {% endblock %} 86 |
87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/add_project.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Add Project 5 | {% endblock %} 6 | 7 | {% block title %} 8 | Add a project 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |
14 |

Add project

15 | {{ form.as_p}} 16 |

17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/build_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Sweet Pony Build Results, yo! 5 | {% endblock %} 6 | 7 | {% block title %} 8 | {{ build }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

{{ build }}

13 |

14 | Success: {{ build.success|yesno:"Passed,Failed" }}
15 | {% if user %} 16 | User: {{ build.user }}
17 | {% endif %} 18 | Host: {{ build.host }}
19 | Arch: {{ build.arch }}
20 | Tags: {% for tag in build.tags %}{{ tag.name }} {% endfor %}
21 | Started: {{ build.started|date:"jS F H:i" }}
22 | Finished: {{ build.finished|date:"jS F H:i" }}
23 |


24 | {% for step in build.steps.all %} 25 | 26 | {{ step.name }}
27 | 28 | {% if step.output %} 29 | Output: 30 |
31 | {{ step.output }}
32 | 
33 | {% endif %} 34 | 35 | {% if step.errout %} 36 | Error Output: 37 |
38 | {{ step.errout }}
39 | 
40 | {% endif %} 41 | {% endfor %} 42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/project_build_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Sweet Pony Build Results, yo! 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Build information

9 |

10 | {{ project }}
11 | {% for build in builds %} 12 | 13 | {{ build }}
14 | {% endfor %} 15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/project_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Sweet Pony Build Results, yo! 5 | {% endblock %} 6 | 7 | {% block title %} 8 | Builds for {{ project }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Build information

13 |

14 | {{ project }}
15 | {% if not project.owner %} 16 | {% if request.user.is_authenticated %} 17 | Claim Project

18 | {% endif %} 19 | {% else %} 20 | Owner: {{ project.owner.username }}

21 | {% endif %} 22 | {% for build in project.builds.all %} 23 | 24 | {{ build }} 25 | | Tags: 26 | {% for tag in build.tags %} 27 | {{ tag }} 28 | {% endfor %} 29 | | {{ build.started|date:"jS F H:i" }} 30 |
31 | {% endfor %} 32 | 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/project_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Latest Results {% endblock %} 4 | 5 | {% block content %} 6 | 7 |

What is this?

8 |

9 | DevMason is a new service that runs your tests when you commit your code. 10 |

11 | 12 |

Latest Builds

13 | 25 | 26 |
27 |

All Projects

28 | {% for project, build in builds.items %} 29 | 38 | {% endfor %} 39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/project_tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Sweet Pony Build Results, yo! 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Build information

9 |

10 | We have build information for:
11 | {{ tag }} 12 | 13 | {% for tag in tags %} 14 | {{ tag }}
15 | 16 | {% endfor %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /devmason_server/templates/devmason_server/tag_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | Sweet Pony Build Results, yo! 5 | {% endblock %} 6 | 7 | {% block title %} 8 | {{ tags.0 }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | {% with tags.0 as tag %} 13 | {% for build in builds %} 14 |

{{ build }}

15 |

16 | Success: {{ build.success|yesno:"Passed,Failed" }}
17 | {% if user %} 18 | User: {{ build.user }}
19 | {% endif %} 20 | Host: {{ build.host }}
21 | Arch: {{ build.arch }}
22 | Tags: {% for tag in build.tags %}{{ tag.name }} {% endfor %}
23 | Started: {{ build.started|date:"jS F H:i" }}
24 | Finished: {{ build.finished|date:"jS F H:i" }}
25 |


26 | {% for step in build.steps.all %} 27 | 28 | {{ step.name }}
29 | {% if step.errout %} 30 | Error Output: 31 |
32 |         {{ step.errout }}
33 |         
34 | {% endif %} 35 | {% if step.output %} 36 | Output: {{ step.output }}
37 | {% endif %} 38 | {% endfor %} 39 | {% endfor %} 40 | {% endwith %} 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/activate.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load humanize %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% if account %} 6 | {% trans "Account activated" %}{% else %}{% trans "Activation problem occurred" %} 7 | {% endif %}{% endblock %} 8 | 9 | {% block page-title %}{% if account %} 10 | {% trans "Account activated" %}{% else %}{% trans "Activation problem occurred" %} 11 | {% endif %}{% endblock %} 12 | 13 | {% block content %}{% if account %} 14 |

{% trans "Thanks for signing up! You can now log in to your account." %}>

15 | 16 |

Log in

17 | 18 | {# @todo: reenable after profiles app in place {% trans "edit your " %}{% trans "profile" %} #} 19 | {% else %} 20 | 21 |

{% blocktrans with expiration_days|apnumber as expiration_days %} 22 | Sorry, a problem occured. Either you already activated your account, your 23 | activation link was incorrect, or the activation key for your account has 24 | expired; activation keys are only valid for {{ expiration_days }} days after 25 | registration. Please contact us if you have any problems. 26 | {% endblocktrans %}

27 | 28 | {% endif %}{% endblock %} 29 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Registration complete" %}{% endblock %} 6 | {% block page-title %}{% trans "Registration complete" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Your account has been activated." %}

10 | {% trans "Go to the homepage." %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | Hello, thanks for registering at Artocrats.com 3 | 4 | To activate your account click on the link below or copy and paste it into your web browser: 5 | 6 | http://{{ site }}/accounts/activate/{{ activation_key }}/ 7 | 8 | Thanks 9 | 10 | *Note: If you didn't request this or no longer want to sign up, you don't need to do anything; you won't receive any more email from us, and the account will expire automatically in {{ expiration_days|apnumber }} days. 11 | 12 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | Activate your new account at {{ site }} 2 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Log in" %}{% endblock %} 5 | {% block page-title %}{% trans "Log in" %}{% endblock %} 6 | 7 | {% block content %} 8 | {% if not user.is_authenticated %} 9 | {% if form.errors %} 10 |

{% trans "Please correct the errors below:" %}{{ form.non_field_errors }}

11 | {% endif %} 12 | 13 |
14 |
15 | 16 | {{ form.username }} 17 | {% if form.username.errors %} {{ form.username.errors|join:", " }}{% endif %} 18 |
19 |
20 | 21 | {{ form.password }} 22 | 23 | {% if form.password.errors %} {{ form.password.errors|join:", " }}{% endif %} 24 |
25 | 26 | 27 | 28 |
29 | {% else %} 30 |

{% trans "You are already logged in as:" %} {{ user.email|escape }}

31 |

{% trans "Log Out" %}

32 | {% endif %} 33 | {% endblock %} 34 | 35 | {% block footer %} 36 | {% if not user.is_authenticated %} 37 |

{% trans "Don't have an account?" %} {% trans "Sign up." %}

38 | {% endif %} 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Logged out" %}{% endblock %} 6 | {% block page-title %}{% trans "Logged out" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Thanks for visiting. Please come back anytime." %}

10 |

{% trans "When you return, don't forget to" %} {% trans "log in" %} {% trans "again." %}

11 | {% endblock %} 12 | 13 | {% block login %}{% endblock %} 14 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password change successful' %}{% endblock %} 5 | {% block page-title %}{% trans 'Password change successful' %}{% endblock %} 6 | 7 | {% block content %} 8 | {% trans 'Password change successful' %} 9 | {% trans 'Your password was changed.' %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password change' %}{% endblock %} 5 | {% block page-title %}{% trans 'Password change' %}{% endblock %} 6 | 7 | {% block content %} 8 | {% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %} 9 |
10 | 11 | {{ form.old_password.errors }} 12 |

{{ form.old_password }}

13 | {{ form.new_password1.errors }} 14 |

{{ form.new_password1 }}

15 | {{ form.new_password2.errors }} 16 |

{{ form.new_password2 }}

17 | 18 |

19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password reset complete' %}{% endblock %} 5 | {% block page-title %}{% trans 'Password reset complete' %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Your password has been set. You may go ahead and log in now." %}

9 | 10 |

{% trans 'Log in' %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password reset' %}{% endblock %} 5 | {% block page-title %}{% trans 'Password reset' %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% if validlink %} 10 |

{% trans 'Enter new password' %}

11 |

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

12 | 13 |
14 | {{ form.new_password1.errors }} 15 |

{{ form.new_password1 }}

16 | {{ form.new_password2.errors }} 17 |

{{ form.new_password2 }}

18 |

19 |
20 | 21 | {% else %} 22 |

{% trans 'Password reset unsuccessful' %}

23 |

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

24 | {% endif %} 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password reset successful' %}{% endblock %} 5 | {% block page-title %}{% trans 'Password reset successful' %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{% trans 'Password reset successful' %}

10 | 11 |

{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}

12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% trans "You're receiving this e-mail because you requested a password reset" %} 3 | {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}. 4 | 5 | {% trans "Please go to the following page and choose a new password:" %} 6 | {% block reset_link %} 7 | {{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %} 8 | {% endblock %} 9 | {% trans "Your username, in case you've forgotten:" %} {{ user.username }} 10 | 11 | {% trans "Thanks for using our site!" %} 12 | 13 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 14 | 15 | {% endautoescape %} 16 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password reset" %}{% endblock %} 5 | {% block page-title %}{% trans "Password reset" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

9 | 10 |
11 | {{ form.email.errors }} 12 |

{{ form.email }}

13 |
14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/registration_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Registration complete" %}{% endblock %} 6 | {% block page-title %}{% trans "Registration complete" %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "An activation link has been sent to the email address you supplied, along with instructions for activating your account." %}

10 | {% trans "Return to homepage." %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /devmason_server/templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Sign up" %}{% endblock %} 5 | {% block page-title %}{% trans "Sign up" %}{% endblock %} 6 | 7 | {% block content %} 8 | {% trans "Please fill out the form to create your account." %} {% trans "You'll then be sent an email with instructions on how to finish your registration." %}

9 | 10 |
11 |
12 | {{ form.as_p }} 13 | 14 |
15 |
16 | {% endblock %} 17 | 18 | {% block footer %} 19 |

{% trans "Already have an account? " %} {% trans "Log in" %}.

20 | {% endblock %} 21 | 22 | {% block login %}{% endblock %} 23 | -------------------------------------------------------------------------------- /devmason_server/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/devmason_server/templatetags/__init__.py -------------------------------------------------------------------------------- /devmason_server/templatetags/capture.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.tag 6 | def capture(parser, token): 7 | """{% capture as [foo] %}""" 8 | bits = token.split_contents() 9 | if len(bits) != 3: 10 | raise template.TemplateSyntaxError("'capture' node requires `as (variable name)`.") 11 | nodelist = parser.parse(('endcapture',)) 12 | parser.delete_first_token() 13 | return CaptureNode(nodelist, bits[2]) 14 | 15 | class CaptureNode(template.Node): 16 | def __init__(self, nodelist, varname): 17 | self.nodelist = nodelist 18 | self.varname = varname 19 | 20 | def render(self, context): 21 | output = self.nodelist.render(context) 22 | context[self.varname] = output 23 | return '' 24 | 25 | -------------------------------------------------------------------------------- /devmason_server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_api import * -------------------------------------------------------------------------------- /devmason_server/tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import difflib 3 | from django.contrib.auth.models import User 4 | from django.test import TestCase, Client 5 | from django.utils import simplejson 6 | from ..models import Build, Project 7 | 8 | class PonyTests(TestCase): 9 | urls = 'devmason_server.urls' 10 | fixtures = ['authtestdata', 'devmason_server_test_data'] 11 | 12 | def setUp(self): 13 | # Make this class's test client have a default Accept: header to 14 | # trigger the dispatching to API methods. 15 | self.client = Client(HTTP_ACCEPT='application/json') 16 | 17 | # Tag us a build (this is hard from fixtures) 18 | b = Build.objects.get(pk=1) 19 | b.tags = 'python, django' 20 | b.save() 21 | 22 | def assertJsonEqual(self, response, expected): 23 | try: 24 | json = simplejson.loads(response.content) 25 | except ValueError: 26 | self.fail('Response was invalid JSON:\n%s' % response) 27 | 28 | # inspired by 29 | # http://code.google.com/p/unittest-ext/source/browse/trunk/unittestnew/case.py#698 30 | self.assert_(isinstance(json, dict), 'JSON response is not a dictionary: \n%r') 31 | self.assert_(isinstance(expected, dict), 'Expected argument is not a dictionary.') 32 | if json != expected: 33 | diff = difflib.ndiff(pprint.pformat(expected).splitlines(), 34 | pprint.pformat(json).splitlines()) 35 | msg = ('\n' + '\n'.join(diff)) 36 | self.fail(msg) 37 | 38 | class PackageListTests(PonyTests): 39 | def test_get_package_list(self): 40 | r = self.client.get('/') 41 | self.assertJsonEqual(r, { 42 | "latest_builds": [ 43 | { 44 | "success": True, 45 | "links": [ 46 | { 47 | "href": "/pony/builds/1", 48 | "allowed_methods": [ 49 | "GET" 50 | ], 51 | "rel": "self" 52 | }, 53 | { 54 | "href": "/pony", 55 | "allowed_methods": [ 56 | "GET", 57 | "PUT", 58 | "DELETE" 59 | ], 60 | "rel": "project" 61 | }, 62 | { 63 | "href": "/pony/tags/django", 64 | "allowed_methods": [ 65 | "GET" 66 | ], 67 | "rel": "tag" 68 | }, 69 | { 70 | "href": "/pony/tags/python", 71 | "allowed_methods": [ 72 | "GET" 73 | ], 74 | "rel": "tag" 75 | } 76 | ], 77 | "tags": [ 78 | "django", 79 | "python" 80 | ], 81 | "started": "Mon, 19 Oct 2009 16:22:00 -0500", 82 | "results": [ 83 | { 84 | "name": "test", 85 | "success": True, 86 | "started": "Mon, 19 Oct 2009 16:21:30 -0500", 87 | "errout": "", 88 | "finished": "Mon, 19 Oct 2009 16:25:00 -0500", 89 | "output": "OK" 90 | }, 91 | { 92 | "name": "checkout", 93 | "success": True, 94 | "started": "Mon, 19 Oct 2009 16:22:00 -0500", 95 | "errout": "", 96 | "finished": "Mon, 19 Oct 2009 16:21:00 -0500", 97 | "output": "OK" 98 | } 99 | ], 100 | "finished": "Mon, 19 Oct 2009 16:25:00 -0500", 101 | "client": { 102 | "host": "example.com", 103 | "arch": "linux-i386", 104 | "user": "" 105 | } 106 | } 107 | ], 108 | "projects": [ 109 | { 110 | "owner": "", 111 | "name": "pony", 112 | "links": [ 113 | { 114 | "href": "/pony", 115 | "allowed_methods": [ 116 | "GET", 117 | "PUT", 118 | "DELETE" 119 | ], 120 | "rel": "self" 121 | }, 122 | { 123 | "href": "/pony/builds", 124 | "allowed_methods": [ 125 | "GET", 126 | "POST" 127 | ], 128 | "rel": "build-list" 129 | }, 130 | { 131 | "href": "/pony/builds/latest", 132 | "allowed_methods": [ 133 | "GET" 134 | ], 135 | "rel": "latest-build" 136 | }, 137 | { 138 | "href": "/pony/tags", 139 | "allowed_methods": [ 140 | "GET" 141 | ], 142 | "rel": "tag-list" 143 | } 144 | ] 145 | } 146 | ], 147 | "builds": { 148 | "pony": { 149 | "success": True, 150 | "links": [ 151 | { 152 | "href": "/pony/builds/1", 153 | "allowed_methods": [ 154 | "GET" 155 | ], 156 | "rel": "self" 157 | }, 158 | { 159 | "href": "/pony", 160 | "allowed_methods": [ 161 | "GET", 162 | "PUT", 163 | "DELETE" 164 | ], 165 | "rel": "project" 166 | }, 167 | { 168 | "href": "/pony/tags/django", 169 | "allowed_methods": [ 170 | "GET" 171 | ], 172 | "rel": "tag" 173 | }, 174 | { 175 | "href": "/pony/tags/python", 176 | "allowed_methods": [ 177 | "GET" 178 | ], 179 | "rel": "tag" 180 | } 181 | ], 182 | "tags": [ 183 | "django", 184 | "python" 185 | ], 186 | "started": "Mon, 19 Oct 2009 16:22:00 -0500", 187 | "results": [ 188 | { 189 | "name": "test", 190 | "success": True, 191 | "started": "Mon, 19 Oct 2009 16:21:30 -0500", 192 | "errout": "", 193 | "finished": "Mon, 19 Oct 2009 16:25:00 -0500", 194 | "output": "OK" 195 | }, 196 | { 197 | "name": "checkout", 198 | "success": True, 199 | "started": "Mon, 19 Oct 2009 16:22:00 -0500", 200 | "errout": "", 201 | "finished": "Mon, 19 Oct 2009 16:21:00 -0500", 202 | "output": "OK" 203 | } 204 | ], 205 | "finished": "Mon, 19 Oct 2009 16:25:00 -0500", 206 | "client": { 207 | "host": "example.com", 208 | "arch": "linux-i386", 209 | "user": "" 210 | } 211 | } 212 | }, 213 | "links": [ 214 | { 215 | "href": "/", 216 | "allowed_methods": [ 217 | "GET" 218 | ], 219 | "rel": "self" 220 | } 221 | ] 222 | } 223 | ) 224 | 225 | class PackageDetailTests(PonyTests): 226 | def test_get_package_detail(self): 227 | r = self.client.get('/pony') 228 | self.assertJsonEqual(r, { 229 | u'name': u'pony', 230 | u'owner': u'', 231 | u'links': [ 232 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 233 | u'href': u'/pony', 234 | u'rel': u'self'}, 235 | {u'allowed_methods': [u'GET', u'POST'], 236 | u'href': u'/pony/builds', 237 | u'rel': u'build-list'}, 238 | {u'allowed_methods': [u'GET'], 239 | u'href': u'/pony/builds/latest', 240 | u'rel': u'latest-build'}, 241 | {u'allowed_methods': [u'GET'], 242 | u'href': u'/pony/tags', 243 | u'rel': u'tag-list'}, 244 | ] 245 | }) 246 | 247 | def test_create_new_package_fails_when_not_authenticated(self): 248 | r = self.client.put('/proj', data='{"name": "myproject"}', 249 | content_type="application/json") 250 | self.assertEqual(r.status_code, 401) # 401 unauthorized 251 | self.assertEqual(r['WWW-Authenticate'], 'Basic realm="pony"') 252 | 253 | def test_create_new_package_works_with_existing_user(self): 254 | auth = "Basic %s" % "testclient:password".encode("base64").strip() 255 | r = self.client.put('/proj', data='{"name": "My Project"}', 256 | content_type="application/json", 257 | HTTP_AUTHORIZATION=auth) 258 | self.assertEqual(r.status_code, 201, r.content) # 201 created 259 | self.assertEqual(r['Location'], 'http://testserver/proj') 260 | 261 | r = self.client.get('/proj') 262 | self.assertJsonEqual(r, { 263 | u'name': u'My Project', 264 | u'owner': u'testclient', 265 | u'links': [ 266 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 267 | u'href': u'/proj', 268 | u'rel': u'self'}, 269 | {u'allowed_methods': [u'GET', u'POST'], 270 | u'href': u'/proj/builds', 271 | u'rel': u'build-list'}, 272 | {u'allowed_methods': [u'GET'], 273 | u'href': u'/proj/builds/latest', 274 | u'rel': u'latest-build'}, 275 | {u'allowed_methods': [u'GET'], 276 | u'href': u'/proj/tags', 277 | u'rel': u'tag-list'}, 278 | ] 279 | }) 280 | 281 | def test_create_new_package_creates_users_automatically(self): 282 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 283 | r = self.client.put('/proj', data='{"name": "My Project"}', 284 | content_type="application/json", 285 | HTTP_AUTHORIZATION=auth) 286 | self.assertEqual(r.status_code, 201, r.content) # 201 created 287 | self.assertEqual(r['Location'], 'http://testserver/proj') 288 | 289 | # Check that the user got created 290 | u = User.objects.get(username='testclient') 291 | self.assertEqual(u.check_password('password'), True) 292 | 293 | def test_update_package_succeeds_when_same_user(self): 294 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 295 | r = self.client.put('/proj', data='{"name": "My Project"}', 296 | content_type="application/json", 297 | HTTP_AUTHORIZATION=auth) 298 | self.assertEqual(r.status_code, 201) # 201 created 299 | 300 | r = self.client.put('/proj', data='{"name": "Renamed"}', 301 | content_type="application/json", 302 | HTTP_AUTHORIZATION=auth) 303 | self.assertJsonEqual(r, { 304 | u'name': u'Renamed', 305 | u'owner': u'newuser', 306 | u'links': [ 307 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 308 | u'href': u'/proj', 309 | u'rel': u'self'}, 310 | {u'allowed_methods': [u'GET', u'POST'], 311 | u'href': u'/proj/builds', 312 | u'rel': u'build-list'}, 313 | {u'allowed_methods': [u'GET'], 314 | u'href': u'/proj/builds/latest', 315 | u'rel': u'latest-build'}, 316 | {u'allowed_methods': [u'GET'], 317 | u'href': u'/proj/tags', 318 | u'rel': u'tag-list'}, 319 | ] 320 | }) 321 | 322 | def test_update_package_fails_when_different_user(self): 323 | # the pony project was created with no auth, so this'll always fail 324 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 325 | r = self.client.put('/pony', data='{"name": "My Project"}', 326 | content_type="application/json", 327 | HTTP_AUTHORIZATION=auth) 328 | self.assertEqual(r.status_code, 403) # 403 forbidden 329 | 330 | def test_delete_package_succeeds_when_user_matches(self): 331 | # Create a package... 332 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 333 | r = self.client.put('/proj', data='{"name": "My Project"}', 334 | content_type="application/json", 335 | HTTP_AUTHORIZATION=auth) 336 | self.assertEqual(r.status_code, 201) # 201 created 337 | 338 | # ... and delete it 339 | r = self.client.delete('/proj', HTTP_AUTHORIZATION=auth) 340 | self.assertEqual(r.status_code, 204) # 204 no content 341 | 342 | def test_delete_package_fails_when_different_user(self): 343 | # Create a package... 344 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 345 | r = self.client.put('/proj', data='{"name": "My Project"}', 346 | content_type="application/json", 347 | HTTP_AUTHORIZATION=auth) 348 | self.assertEqual(r.status_code, 201) # 201 created 349 | 350 | # ... and delete it with different auth 351 | auth = "Basic %s" % "testclient:password".encode("base64").strip() 352 | r = self.client.delete('/proj', HTTP_AUTHORIZATION=auth) 353 | self.assertEqual(r.status_code, 403) # 403 forbidden 354 | 355 | def test_delete_package_should_not_create_users(self): 356 | # Create a package... 357 | auth = "Basic %s" % "newuser:password".encode("base64").strip() 358 | r = self.client.put('/proj', data='{"name": "My Project"}', 359 | content_type="application/json", 360 | HTTP_AUTHORIZATION=auth) 361 | self.assertEqual(r.status_code, 201) # 201 created 362 | 363 | # ... and delete it with a new user 364 | auth = "Basic %s" % "newuwer2:password".encode("base64").strip() 365 | r = self.client.delete('/proj', HTTP_AUTHORIZATION=auth) 366 | self.assertEqual(r.status_code, 403) # 403 forbidden 367 | 368 | # newuser2 shouldn't have been created 369 | self.assertRaises(User.DoesNotExist, User.objects.get, username='newuser2') 370 | 371 | class BuildListTests(PonyTests): 372 | def test_get_package_build_list(self): 373 | r = self.client.get('/pony/builds') 374 | self.assertJsonEqual(r, { 375 | u'count': 1, 376 | u'num_pages': 1, 377 | u'page': 1, 378 | u'paginated': False, 379 | u'per_page': 25, 380 | u'project': { 381 | u'name': u'pony', 382 | u'owner': u'', 383 | u'links': [ 384 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 385 | u'href': u'/pony', 386 | u'rel': u'self'}, 387 | {u'allowed_methods': [u'GET', u'POST'], 388 | u'href': u'/pony/builds', 389 | u'rel': u'build-list'}, 390 | {u'allowed_methods': [u'GET'], 391 | u'href': u'/pony/builds/latest', 392 | u'rel': u'latest-build'}, 393 | {u'allowed_methods': [u'GET'], 394 | u'href': u'/pony/tags', 395 | u'rel': u'tag-list'} 396 | ], 397 | }, 398 | u'builds': [{ 399 | u'success': True, 400 | u'started': u'Mon, 19 Oct 2009 16:22:00 -0500', 401 | u'finished': u'Mon, 19 Oct 2009 16:25:00 -0500', 402 | u'tags': [u'django', u'python'], 403 | u'client': {u'host': u'example.com', u'user': u'', u'arch': u'linux-i386'}, 404 | u'results': [ 405 | {u'errout': u'', 406 | u'finished': u'Mon, 19 Oct 2009 16:25:00 -0500', 407 | u'name': u'test', 408 | u'output': u'OK', 409 | u'started': u'Mon, 19 Oct 2009 16:21:30 -0500', 410 | u'success': True}, 411 | {u'errout': u'', 412 | u'finished': u'Mon, 19 Oct 2009 16:21:00 -0500', 413 | u'name': u'checkout', 414 | u'output': u'OK', 415 | u'started': u'Mon, 19 Oct 2009 16:22:00 -0500', 416 | u'success': True} 417 | ], 418 | u'links': [ 419 | {u'allowed_methods': [u'GET'], 420 | u'href': u'/pony/builds/1', 421 | u'rel': u'self'}, 422 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 423 | u'href': u'/pony', 424 | u'rel': u'project'}, 425 | {u'allowed_methods': [u'GET'], 426 | u'href': u'/pony/tags/django', 427 | u'rel': u'tag'}, 428 | {u'allowed_methods': [u'GET'], 429 | u'href': u'/pony/tags/python', 430 | u'rel': u'tag'} 431 | ] 432 | }], 433 | u'links': [ 434 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 435 | u'href': u'/pony', 436 | u'rel': u'project'}, 437 | {u'allowed_methods': [u'GET'], 438 | u'href': u'/pony/builds/latest', 439 | u'rel': u'latest-build'}, 440 | {u'allowed_methods': [u'GET'], 441 | u'href': u'/pony/builds?per_page=25&page=1', 442 | u'rel': u'self'}, 443 | ] 444 | }) 445 | 446 | def test_post_new_build(self): 447 | build = { 448 | u'success': False, 449 | u'started': u'Mon, 26 Oct 2009 16:22:00 -0500', 450 | u'finished': u'Mon, 26 Oct 2009 16:25:00 -0500', 451 | u'tags': [u'pony', u'build', u'rocks'], 452 | u'client': {u'host': u'example.com', u'user': u'', u'arch': u'linux-i386', u'extra': u'hi!'}, 453 | u'results': [ 454 | {u'errout': u'', 455 | u'finished': u'Mon, 26 Oct 2009 16:25:00 -0500', 456 | u'name': u'test', 457 | u'output': u'Step 1: OK', 458 | u'started': u'Mon, 26 Oct 2009 16:21:30 -0500', 459 | u'success': True}, 460 | {u'errout': u'', 461 | u'finished': u'Mon, 26 Oct 2009 16:21:00 -0500', 462 | u'name': u'checkout', 463 | u'output': u'Step 2: OK', 464 | u'started': u'Mon, 26 Oct 2009 16:22:00 -0500', 465 | u'success': False} 466 | ], 467 | } 468 | 469 | r = self.client.post('/pony/builds', data=simplejson.dumps(build), 470 | content_type='application/json') 471 | self.assertEqual(r.status_code, 201, r) # 201 created 472 | self.assertEqual(r['Location'], 'http://testserver/pony/builds/2') 473 | 474 | # make sure the build info came through okay 475 | r = self.client.get('/pony/builds/2') 476 | #Enable this again with a mock date time for finished. 477 | """ 478 | self.assertJsonEqual(r, { 479 | u'success': False, 480 | u'started': u'Mon, 26 Oct 2009 16:22:00 -0500', 481 | u'finished': u'Mon, 26 Oct 2009 16:25:00 -0500', 482 | u'tags': [u'build', u'pony', u'rocks'], 483 | u'client': {u'host': u'example.com', u'user': u'', u'arch': u'linux-i386', u'extra': u'hi!'}, 484 | u'results': [ 485 | {u'errout': u'', 486 | u'finished': u'Mon, 26 Oct 2009 16:25:00 -0500', 487 | u'name': u'test', 488 | u'output': u'Step 1: OK', 489 | u'started': u'Mon, 26 Oct 2009 16:21:30 -0500', 490 | u'success': True}, 491 | {u'errout': u'', 492 | u'finished': u'Mon, 26 Oct 2009 16:21:00 -0500', 493 | u'name': u'checkout', 494 | u'output': u'Step 2: OK', 495 | u'started': u'Mon, 26 Oct 2009 16:22:00 -0500', 496 | u'success': False} 497 | ], 498 | u'links': [ 499 | {u'allowed_methods': [u'GET'], 500 | u'href': u'/pony/builds/2', 501 | u'rel': u'self'}, 502 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 503 | u'href': u'/pony', 504 | u'rel': u'project'}, 505 | {u'allowed_methods': [u'GET'], 506 | u'href': u'/pony/tags/build', 507 | u'rel': u'tag'}, 508 | {u'allowed_methods': [u'GET'], 509 | u'href': u'/pony/tags/pony', 510 | u'rel': u'tag'}, 511 | {u'allowed_methods': [u'GET'], 512 | u'href': u'/pony/tags/rocks', 513 | u'rel': u'tag'} 514 | ], 515 | }) 516 | """ 517 | 518 | class LatestBuildTests(PonyTests): 519 | def test_get_latest_build(self): 520 | r = self.client.get('/pony/builds/latest') 521 | self.assertEqual(r.status_code, 302) 522 | self.assertEqual(r['Location'], 'http://testserver/pony/builds/1') 523 | 524 | class TagListTests(PonyTests): 525 | def test_get_tag_list(self): 526 | r = self.client.get('/pony/tags') 527 | self.assertJsonEqual(r, { 528 | u'tags': [u'django', u'python'], 529 | u'links': [ 530 | {u'allowed_methods': [u'GET'], 531 | u'href': u'/pony/tags', 532 | u'rel': u'self'}, 533 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 534 | u'href': u'/pony', 535 | u'rel': u'project'}, 536 | {u'allowed_methods': [u'GET'], 537 | u'href': u'/pony/tags/django', 538 | u'rel': u'tag'}, 539 | {u'allowed_methods': [u'GET'], 540 | u'href': u'/pony/tags/python', 541 | u'rel': u'tag'} 542 | ] 543 | }) 544 | 545 | class TagDetailTests(PonyTests): 546 | def test_get_tag_detail(self): 547 | r = self.client.get('/pony/tags/django') 548 | self.assertJsonEqual(r, { 549 | u'count': 1, 550 | u'num_pages': 1, 551 | u'page': 1, 552 | u'paginated': False, 553 | u'per_page': 25, 554 | u'tags': [u'django'], 555 | u'builds': [{ 556 | u'success': True, 557 | u'started': u'Mon, 19 Oct 2009 16:22:00 -0500', 558 | u'finished': u'Mon, 19 Oct 2009 16:25:00 -0500', 559 | u'tags': [u'django', u'python'], 560 | u'client': {u'host': u'example.com', u'user': u'', u'arch': u'linux-i386'}, 561 | u'results': [ 562 | {u'errout': u'', 563 | u'finished': u'Mon, 19 Oct 2009 16:25:00 -0500', 564 | u'name': u'test', 565 | u'output': u'OK', 566 | u'started': u'Mon, 19 Oct 2009 16:21:30 -0500', 567 | u'success': True}, 568 | {u'errout': u'', 569 | u'finished': u'Mon, 19 Oct 2009 16:21:00 -0500', 570 | u'name': u'checkout', 571 | u'output': u'OK', 572 | u'started': u'Mon, 19 Oct 2009 16:22:00 -0500', 573 | u'success': True} 574 | ], 575 | u'links': [ 576 | {u'allowed_methods': [u'GET'], 577 | u'href': u'/pony/builds/1', 578 | u'rel': u'self'}, 579 | {u'allowed_methods': [u'GET', u'PUT', u'DELETE'], 580 | u'href': u'/pony', 581 | u'rel': u'project'}, 582 | {u'allowed_methods': [u'GET'], 583 | u'href': u'/pony/tags/django', 584 | u'rel': u'tag'}, 585 | {u'allowed_methods': [u'GET'], 586 | u'href': u'/pony/tags/python', 587 | u'rel': u'tag'} 588 | ] 589 | }], 590 | u'links': [ 591 | {u'allowed_methods': [u'GET'], 592 | u'href': u'/pony/tags/django?per_page=25&page=1', 593 | u'rel': u'self'}, 594 | ] 595 | }) 596 | 597 | class LatestTagsTests(PonyTests): 598 | def test_get_latest_tagged_build(self): 599 | r = self.client.get('/pony/tags/django/latest') 600 | self.assertEqual(r.status_code, 302) 601 | self.assertEqual(r['Location'], 'http://testserver/pony/builds/1') 602 | 603 | def test_get_latest_tagged_build_404s_with_invalid_tags(self): 604 | r = self.client.get('/pony/tags/nope/latest') 605 | self.assertEqual(r.status_code, 404) -------------------------------------------------------------------------------- /devmason_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from .utils import Resource 3 | from . import handlers 4 | 5 | urlpatterns = patterns('', 6 | url(r'^$', 7 | Resource(handlers.ProjectListHandler), 8 | name = 'project_list' 9 | ), 10 | url(r'builds/github', 11 | 'devmason_server.views.github_build', 12 | name='github_build' 13 | ), 14 | url(r'builds/bitbucket', 15 | 'devmason_server.views.bitbucket_build', 16 | name='bitbucket_build' 17 | ), 18 | url(r'builds/request', 19 | 'devmason_server.views.request_build', 20 | name='request_build' 21 | ), 22 | url(r'add_project', 23 | 'devmason_server.views.add_project', 24 | name='project_add' 25 | ), 26 | url(r'xmlrpc', 27 | 'devmason_server.views.xmlrpc', 28 | name='xmlrpc' 29 | ), 30 | url(r'^(?P[\w-]+)$', 31 | Resource(handlers.ProjectHandler), 32 | name = 'project_detail' 33 | ), 34 | url(r'^(?P[\w-]+)/claim$', 35 | 'devmason_server.views.claim_project', 36 | name = 'claim_project' 37 | ), 38 | url(r'^(?P[\w-]+)/builds$', 39 | Resource(handlers.ProjectBuildListHandler), 40 | name = 'project_build_list' 41 | ), 42 | url(r'^(?P[\w-]+)/builds/(?P\d+)$', 43 | Resource(handlers.BuildHandler), 44 | name = 'build_detail' 45 | ), 46 | url(r'^(?P[\w-]+)/builds/latest$', 47 | Resource(handlers.LatestBuildHandler), 48 | name = 'latest_build' 49 | ), 50 | url(r'^(?P[\w-]+)/tags$', 51 | Resource(handlers.ProjectTagListHandler), 52 | name = 'project_tag_list' 53 | ), 54 | url(r'^(?P[\w-]+)/tags/(?P[^/]+)/latest$', 55 | Resource(handlers.ProjectLatestTaggedBuildHandler), 56 | name = 'latest_tagged_build' 57 | ), 58 | url(r'^(?P[\w-]+)/tags/(?P.*)$', 59 | Resource(handlers.TagHandler), 60 | name = 'tag_detail' 61 | ), 62 | ) 63 | -------------------------------------------------------------------------------- /devmason_server/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Piston API helpers. 3 | """ 4 | 5 | import unicodedata 6 | import re 7 | import time 8 | import datetime 9 | import functools 10 | import mimeparse 11 | import email.utils 12 | import piston.resource 13 | import piston.emitters 14 | import piston.handler 15 | import piston.utils 16 | from django.db import models 17 | from django.contrib.auth.models import User, AnonymousUser 18 | from django.core import urlresolvers 19 | from django.http import (Http404, HttpResponse, HttpResponseRedirect, 20 | HttpResponseForbidden) 21 | from django.shortcuts import render_to_response 22 | from django.template import RequestContext 23 | from django.utils import dateformat 24 | from django.utils.http import urlencode 25 | 26 | # Try to use dateutil for maximum date-parsing niceness. Fall back to 27 | # hard-coded RFC2822 parsing if that's not possible. 28 | try: 29 | import dateutil.parser 30 | except ImportError: 31 | dateutil = None 32 | 33 | class Resource(piston.resource.Resource): 34 | """Chooses an emitter based on mime types.""" 35 | 36 | def determine_emitter(self, request, *args, **kwargs): 37 | # First look for a format hardcoded into the URLconf 38 | em = kwargs.pop('emitter_format', None) 39 | 40 | # Then look for ?format=json 41 | if not em: 42 | em = request.GET.get('format', None) 43 | 44 | # Finally fall back on HTML 45 | return em or 'html' 46 | 47 | class HTMLTemplateEmitter(piston.emitters.Emitter): 48 | """Emit a resource using a good old fashioned template.""" 49 | 50 | def render(self, request): 51 | if isinstance(self.data, HttpResponse): 52 | return self.data 53 | if isinstance(self.data, models.Model): 54 | context = {self.data._meta.object_name.lower(): self.data, 55 | 'links': self.construct()['links']} 56 | else: 57 | context = self.data 58 | return render_to_response( 59 | 'devmason_server/%s.html' % self.handler.viewname.lower(), 60 | context, 61 | context_instance = RequestContext(request), 62 | ) 63 | 64 | piston.emitters.Emitter.register('html', HTMLTemplateEmitter, 'text/html') 65 | 66 | class HttpResponseUnauthorized(HttpResponse): 67 | status_code = 401 68 | 69 | def __init__(self): 70 | HttpResponse.__init__(self) 71 | self['WWW-Authenticate'] = 'Basic realm="pony"' 72 | 73 | class HttpResponseCreated(HttpResponseRedirect): 74 | status_code = 201 75 | 76 | class HttpResponseNoContent(HttpResponse): 77 | status_code = 204 78 | 79 | def link(rel, to_handler, *args, **getargs): 80 | """ 81 | Create a link resource - a dict with rel, href, and allowed_methods keys. 82 | Extra args taken by the view may be passed in as *args; GET may be passed 83 | as **kwargs. 84 | """ 85 | href = urlresolvers.reverse(to_handler.viewname, args=args) 86 | if getargs: 87 | href = "%s?%s" % (href, urlencode(getargs)) 88 | return { 89 | 'rel': rel, 90 | 'href': href, 91 | 'allowed_methods': to_handler.allowed_methods 92 | } 93 | 94 | # Needed now; will be fixed in Piston 0.2.3 95 | def allow_404(func): 96 | """ 97 | decorator that catches Http404 exceptions and safely returns 98 | piston style 404 responses (rc.NOT_FOUND). 99 | """ 100 | def wrapper(*args, **kwargs): 101 | try: 102 | return func(*args, **kwargs) 103 | except Http404: 104 | return piston.utils.rc.NOT_FOUND 105 | return wrapper 106 | 107 | def _get_user(request): 108 | """ 109 | Pull a user out of the `Authorization` header. Returns the user, an 110 | `AnonymousUser` if there's no Authorization header, or `None` if there was 111 | an invalid `Authorization` header. 112 | """ 113 | if 'HTTP_AUTHORIZATION' not in request.META or not request.META['HTTP_AUTHORIZATION']: 114 | return AnonymousUser(), None 115 | 116 | # Get or create a user from the Authorization header. 117 | try: 118 | authtype, auth = request.META['HTTP_AUTHORIZATION'].split(' ') 119 | if authtype.lower() != 'basic': 120 | return None 121 | username, password = auth.decode('base64').split(':') 122 | user = User.objects.get(username=username) 123 | user.is_new_user = False 124 | except ValueError: 125 | # Raised if split()/unpack fails 126 | return None 127 | except User.DoesNotExist: 128 | user = User(username=username) 129 | user.set_password(password) 130 | user.is_new_user = True 131 | 132 | return user, password 133 | 134 | def authentication_required(callback): 135 | """ 136 | Require that a handler method be called with authentication. 137 | 138 | Pony server has somewhat "interesting" authentication: new users are 139 | created transparently when creating new resources, so this needs to keep 140 | track of whether a user is "new" or not so that the handler may optionally 141 | save the user if needed. Thus, this annotates `request.user` with the user 142 | (new or not), and sets a `is_new_user` attribute on this user. 143 | """ 144 | @functools.wraps(callback) 145 | def _view(self, request, *args, **kwargs): 146 | user, password = _get_user(request) 147 | if not user or user.is_anonymous(): 148 | return HttpResponseUnauthorized() 149 | if not user.check_password(password): 150 | return HttpResponseForbidden() 151 | request.user = user 152 | return callback(self, request, *args, **kwargs) 153 | return _view 154 | 155 | def authentication_optional(callback): 156 | """ 157 | Optionally allow authentication for a view. 158 | 159 | Like `authentication_required`, except that if there's no auth info then 160 | `request.user` will be `AnonymousUser`. 161 | """ 162 | @functools.wraps(callback) 163 | def _view(self, request, *args, **kwargs): 164 | user, password = _get_user(request) 165 | if not user: 166 | return HttpResponseUnauthorized() 167 | if user.is_authenticated() and not user.check_password(password): 168 | return HttpResponseForbidden() 169 | request.user = user 170 | return callback(self, request, *args, **kwargs) 171 | return _view 172 | 173 | def format_dt(dt): 174 | return dateformat.format(dt, 'r') 175 | 176 | # Try to use dateutil for maximum date-parsing niceness. Fall back to 177 | # hard-coded RFC2822 parsing if that's not possible. 178 | if dateutil: 179 | mk_datetime = dateutil.parser.parse 180 | else: 181 | def mk_datetime(string): 182 | return datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string))) 183 | 184 | def slugify(value): 185 | """ 186 | Normalizes string, converts to lowercase, removes non-alpha characters, 187 | and converts spaces to hyphens. 188 | """ 189 | value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') 190 | value = unicode(re.sub('[^\w\s-]', '_', value).strip().lower()) 191 | return re.sub('[-\s]+', '-', value) 192 | 193 | 194 | -------------------------------------------------------------------------------- /devmason_server/views.py: -------------------------------------------------------------------------------- 1 | try: 2 | import json 3 | except: 4 | import simplejson as json 5 | 6 | import datetime 7 | from SimpleXMLRPCServer import SimpleXMLRPCDispatcher 8 | 9 | from django.shortcuts import render_to_response 10 | from django.http import HttpResponse, HttpResponseRedirect 11 | from django.core.handlers.wsgi import WSGIRequest 12 | from django.template import RequestContext 13 | 14 | 15 | from devmason_server.models import Repository, BuildRequest, Project 16 | from devmason_server.handlers import ProjectBuildListHandler 17 | from devmason_server.utils import slugify 18 | from devmason_server.forms import ProjectForm 19 | 20 | def add_project(request, template_name='devmason_server/add_project.html'): 21 | """ 22 | Add project 23 | 24 | Template: ``projects/new_project.html`` 25 | Context: 26 | form 27 | Form object. 28 | """ 29 | form = ProjectForm(request.POST or None) 30 | if form.is_valid(): 31 | project, created = Project.objects.get_or_create(name=form.cleaned_data['name'], 32 | slug=slugify(form.cleaned_data['name'])) 33 | repo, created = Repository.objects.get_or_create( 34 | url=form.cleaned_data['source_repo'], 35 | project=project, 36 | ) 37 | return HttpResponseRedirect(project.get_absolute_url()) 38 | return render_to_response(template_name, {'form': form}, 39 | context_instance=RequestContext(request)) 40 | 41 | def claim_project(request, slug): 42 | project = Project.objects.get(slug=slug) 43 | project.owner = request.user 44 | project.save() 45 | return HttpResponse('Project has been claimed') 46 | 47 | def github_build(request): 48 | obj = json.loads(request.POST['payload']) 49 | name = obj['repository']['name'] 50 | url = obj['repository']['url'] 51 | git_url = url.replace('http://', 'git://') 52 | hash = obj['after'] 53 | 54 | project = Project.objects.get(slug=name) 55 | repo, created = Repository.objects.get_or_create( 56 | url=git_url, 57 | project=project, 58 | type='git', 59 | ) 60 | brequest = BuildRequest.objects.create( 61 | repository = repo, 62 | identifier = hash, 63 | requested = datetime.datetime.utcnow(), 64 | ) 65 | return HttpResponse('Build Started') 66 | 67 | def bitbucket_build(request): 68 | obj = json.loads(request.POST['payload']) 69 | rep = obj['repository'] 70 | name = rep['name'] 71 | url = "%s%s" % ("http://bitbucket.org", rep['absolute_url']) 72 | hash = obj['commits'][0]['node'] 73 | 74 | project = Project.objects.get(slug=name) 75 | repo, created = Repository.objects.get_or_create( 76 | url=url, 77 | project=project, 78 | type='hg', 79 | ) 80 | brequest = BuildRequest.objects.create( 81 | repository = repo, 82 | identifier = hash, 83 | requested = datetime.datetime.utcnow(), 84 | ) 85 | return HttpResponse('Build Started') 86 | 87 | def request_build(request): 88 | obj = json.loads(request.raw_post_data) 89 | project = obj['project'] 90 | identifier = obj['identifier'] 91 | repo = Repository.objects.get(project__slug=project) 92 | brequest = BuildRequest.objects.create( 93 | repository = repo, 94 | identifier = identifier, 95 | requested = datetime.datetime.utcnow(), 96 | ) 97 | return HttpResponse('Build Started') 98 | 99 | ### Crazy XMLRPC stuff below here. 100 | 101 | # Create a Dispatcher; this handles the calls and translates info to function maps 102 | dispatcher = SimpleXMLRPCDispatcher(allow_none=False, encoding=None) # Python 2.5 103 | 104 | def xmlrpc(request): 105 | response = HttpResponse() 106 | if len(request.POST): 107 | response.write(dispatcher._marshaled_dispatch(request.raw_post_data)) 108 | else: 109 | response.write("This is an XML-RPC Service.
") 110 | response.write("You need to invoke it using an XML-RPC Client!
") 111 | response.write("The following methods are available:
    ") 112 | methods = dispatcher.system_listMethods() 113 | 114 | for method in methods: 115 | sig = dispatcher.system_methodSignature(method) 116 | help = dispatcher.system_methodHelp(method) 117 | response.write("
  • %s: [%s] %s" % (method, sig, help)) 118 | response.write("
") 119 | response.write(' Made with Django.') 120 | response['Content-length'] = str(len(response.content)) 121 | return response 122 | 123 | 124 | 125 | def add_results(info, results): 126 | "Return sweet results" 127 | build_dict = {'success': info.get('success', False), 128 | 'started': info.get('start_time', ''), 129 | 'finished': info.get('end_time', ''), 130 | 'tags': info['tags'], 131 | 'client': { 132 | 'arch': info.get('arch', ''), 133 | 'host':info.get('host', ''), 134 | 'user': 'pony-client', 135 | }, 136 | 'results': [] 137 | } 138 | 139 | for result in results: 140 | success = False 141 | #Status code of 0 means successful 142 | if result.get('status', False) == 0: 143 | success = True 144 | build_dict['results'].append( 145 | {'success': success, 146 | 'name': result.get('name', ''), 147 | 'errout': result.get('errout', ''), 148 | 'output': result.get('output', ''), 149 | 'command': result.get('command', ''), 150 | 'type': result.get('type', ''), 151 | 'version_type': result.get('version_type', ''), 152 | 'version_info': result.get('version_info', ''), 153 | } 154 | ) 155 | 156 | environ = { 157 | 'PATH_INFO': '/', 158 | 'QUERY_STRING': '', 159 | 'REQUEST_METHOD': 'GET', 160 | 'SCRIPT_NAME': '', 161 | 'SERVER_NAME': 'testserver', 162 | 'SERVER_PORT': 80, 163 | 'SERVER_PROTOCOL': 'HTTP/1.1', 164 | } 165 | r = WSGIRequest(environ) 166 | r.data = build_dict 167 | r.META['CONTENT_TYPE'] = 'application/json' 168 | package = unicode(info.get('package')) 169 | try: 170 | pro, created = Project.objects.get_or_create(name=package, slug=slugify(package)) 171 | except: 172 | pass 173 | ProjectBuildListHandler().create(r, package) 174 | return "Processed Correctly" 175 | 176 | 177 | 178 | def check_should_build(client_info, True, reserve_time): 179 | return (True, "We always build, now!") 180 | 181 | dispatcher.register_function(add_results, 'add_results') 182 | dispatcher.register_function(check_should_build, 'check_should_build') 183 | -------------------------------------------------------------------------------- /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 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DevmasonServer.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DevmasonServer.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /docs/build-server-rest-api.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Build server REST API 3 | ===================== 4 | 5 | This is a proposed standard for a REST API for build clients to use to 6 | communicate with a build server. It's inspired by pony-build, and generally 7 | rather Python-oriented, but the goal is language-agnostic. 8 | 9 | .. contents:: Contents 10 | 11 | API Usage 12 | ========= 13 | 14 | Registering a new project 15 | ------------------------- 16 | 17 | .. parsed-literal:: 18 | 19 | -> PUT /{project} 20 | 21 | {Project_} 22 | 23 | <- 201 Created 24 | Location: /{project}/builds/{build-id} 25 | 26 | If a project already exists, a 403 Forbidden will be returned. 27 | 28 | Users may register with authentication via HTTP Basic:: 29 | 30 | -> PUT /{project} 31 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== 32 | 33 | {Project_} 34 | 35 | <- 201 Created 36 | Location: /{project}/builds/{build-id} 37 | 38 | If this is done, then that authorization may be repeated in the future to 39 | update/delete the project or to delete builds. No explicit user registration 40 | step is needed; users will be created on the fly. 41 | 42 | .. warning:: 43 | 44 | Since the authorization uses HTTP Basic, build servers should probably 45 | support SSL for the security-conscious. 46 | 47 | Reporting a build 48 | ----------------- 49 | 50 | .. parsed-literal:: 51 | 52 | -> POST /{project}/builds 53 | 54 | {Build_} 55 | 56 | <- 201 Created 57 | Location: /{project}/builds/{build-id} 58 | 59 | Incremental build reporting 60 | --------------------------- 61 | 62 | .. parsed-literal:: 63 | 64 | -> POST /{project}/builds 65 | 66 | {`Incremental build`_} 67 | 68 | <- 201 Created 69 | Location: /{project}/builds/{build-id}/progress 70 | 71 | -> POST /{project}/builds/{build-id}/progress 72 | 73 | {`Build step`_} 74 | 75 | <- 204 No Content 76 | Location: /{project}/builds/{build-id} 77 | 78 | -> POST /{project}/builds/{build-id}/progress 79 | 80 | {`Build step`_} 81 | 82 | <- 204 No Content 83 | Location: /{project}/builds/{build-id} 84 | 85 | 86 | ... 87 | 88 | -> DELETE /{project}/builds/{build-id}/progress 89 | 90 | <- 204 No Content 91 | Location: /{project}/builds/{build-id} 92 | 93 | API Reference 94 | ============= 95 | 96 | Representation formats 97 | ---------------------- 98 | 99 | * JSON. 100 | * UTF-8. 101 | * All datetimes in RFC 2822. 102 | 103 | URIs 104 | ---- 105 | 106 | ============================================== ================== ================== ======================================== 107 | URI Resource Methods Notes 108 | ============================================== ================== ================== ======================================== 109 | ``/`` `Project list`_ GET 110 | 111 | ``/{project}`` Project_ ``GET``, ``PUT``, Only the user that created a project may 112 | ``DELETE`` update (``PUT``) or delete it. 113 | 114 | ``/{project}/builds`` `Build list`_ ``GET``, ``POST`` 115 | 116 | ``/{project}/builds/latest`` -- ``GET`` 302 redirect to latest build. 117 | 118 | ``/{project}/builds/{build-id}`` Build_ ``GET``, ``PUT``, Builds may not be updated; ``PUT`` only 119 | ``DELETE`` exists if clients wish for some reason 120 | to use a predetermined build id. Only 121 | the user that created a build or the 122 | project owner may delete a build. 123 | 124 | ``/{project}/builds/{build-id}/progress`` `Build progress`_ ``GET``, ``POST``, 125 | ``DELETE`` 126 | 127 | ``/{project}/tags`` `Tag list`_ ``GET`` 128 | 129 | ``/{project}/tags/{-listjoin|-|tags}`` `Build list`_ ``GET`` 130 | 131 | ``/{project}/tags/{-listjoin|-|tags}/latest`` -- ``GET`` 302 redirect to latest build given tags 132 | 133 | ``/users`` `User list`_ ``GET`` 134 | 135 | ``/users/{username}`` `User`_ ``GET``, ``PUT``, Authentication required to ``PUT`` or 136 | ``DELETE`` ``DELETE``. 137 | 138 | ``/users/{username}/builds`` `Build list`_ ``GET`` 139 | 140 | ``/users/{username}/builds/latest`` -- ``GET`` 302 redirect to latest build by user 141 | ============================================== ================== ================== ======================================== 142 | 143 | All resources support ``OPTIONS`` which will return a list of allowed methods 144 | in the ``Allow`` header. This is particularly useful to check authentication 145 | for methods that require it. 146 | 147 | Resources 148 | --------- 149 | 150 | Build 151 | ~~~~~ 152 | 153 | Representation: 154 | 155 | .. parsed-literal:: 156 | 157 | { 158 | 'success': true, # did the build succeed? 159 | 'started': 'Tue, 20 Oct 2009 10:20:00 -0500', 160 | 'finished': 'Tue, 20 Oct 2009 10:22:00 -0500, 161 | 162 | 'tags': ['list', 'of', 'tags'], 163 | 164 | 'client': { 165 | 'host': 'example.com', # host that ran the build 166 | 'user': 'http://example.com/' # user to credit for build. 167 | 'arch': 'macosx-10.5-i386' # architecture the build was done on. 168 | ... [#]_ 169 | }, 170 | 171 | 'results': [{`Build step`_}, ...], 172 | 173 | 'links': [{Link_}, ...] 174 | } 175 | 176 | Notes: 177 | 178 | .. [#] Clients may include arbitrary extra client info in the client record. 179 | 180 | Links: 181 | 182 | =========== ====================================================== 183 | Rel Links to 184 | =========== ====================================================== 185 | ``self`` This `build`_ 186 | ``project`` The project_ this is a builds of. 187 | ``tag`` A tag_ this build is tagged with. There'll probably be 188 | many ``tag`` links. 189 | =========== ====================================================== 190 | 191 | Build list 192 | ~~~~~~~~~~ 193 | 194 | Representation: 195 | 196 | .. parsed-literal:: 197 | 198 | { 199 | 'builds': [{Build_}, ...], 200 | 201 | 'count': 100, # total number of builds available 202 | 'num_pages': 4, # total number of pages 203 | 'page': 1 # current page number 204 | 'paginated': true # is this list paginated? 205 | 'per_page': 25, # number of builds per page 206 | 207 | 'links': [{Link_, ...}] 208 | } 209 | 210 | Links: 211 | 212 | ================ ========================================================== 213 | Rel Links to 214 | ================ ========================================================== 215 | ``self`` This `build list`_ 216 | ``project`` The project_ this is a list of builds for (if applicable). 217 | ``user`` The user_ this is a list of builds for (if applicable). 218 | ``tag`` The tag_ this is a list of builds for (if applicable). 219 | ``latest-build`` URI for the redirect to this project's latest build. 220 | ``next`` The next page of builds (if applicable). 221 | ``previous`` The previous page of builds (if applicable). 222 | ``first`` The first page of builds. 223 | ``last`` The last page of builds. 224 | ================ ========================================================== 225 | 226 | Build progress 227 | ~~~~~~~~~~~~~~ 228 | 229 | Used as an entry point for `incremental build reporting`_ 230 | 231 | Empty representation -- the existence of the resource indicates an in-progress 232 | build. When the build is done, the resource will return 410 Gone. 233 | 234 | Build step 235 | ~~~~~~~~~~ 236 | 237 | Representation: 238 | 239 | .. parsed-literal:: 240 | 241 | { 242 | 'success': true, # did this step succeed? 243 | 'started': 'Tue, 20 Oct 2009 10:20:00 -0500', 244 | 'finished': 'Tue, 20 Oct 2009 10:22:00 -0500, 245 | 'name': 'checkout', # human-readable name for the step 246 | 'output': '...' # stdout for this step 247 | 'errout': '...' # stderr for this step 248 | ... [#]_ 249 | } 250 | 251 | Notes: 252 | 253 | .. [#] Build steps may include arbitrary extra build info in the record. 254 | \ 255 | 256 | Incremental build 257 | ~~~~~~~~~~~~~~~~~ 258 | 259 | ``POST`` this resource to a `build list`_ to signal the start of an incremental build. 260 | 261 | Representation 262 | 263 | .. parsed-literal:: 264 | 265 | { 266 | 'incremental': true, # never false 267 | 'started': 'Tue, 20 Oct 2009 10:20:00 -0500', # when the build started on 268 | # the client (not when the 269 | # packet was posted!) 270 | 'client': { 271 | 'host': 'example.com', # host that ran the build 272 | 'user': 'username' # user to credit for build. 273 | 'arch': 'macosx-10.5-i386' # architecture the build was done on. 274 | ... [#]_ 275 | }, 276 | 277 | 'tags': ['list', 'of', 'tags'], 278 | } 279 | 280 | Notes: 281 | 282 | .. [#] Clients may include arbitrary extra client info in the client record. 283 | 284 | Link 285 | ~~~~ 286 | 287 | Used all over the damn place to knit resources together. 288 | 289 | Representation:: 290 | 291 | { 292 | 'rel': 'self', # identifier for the type of link this is 293 | 'href': 'http://example.com/', # full URL href 294 | 'allowed_methods': ['GET'], # list of methods this client can perform on said resource 295 | } 296 | 297 | 298 | Project 299 | ~~~~~~~ 300 | 301 | Representation: 302 | 303 | .. parsed-literal:: 304 | 305 | { 306 | 'name': 'Project Name', 307 | 'owner': 'username', # the user who created the project, if applicable. 308 | 309 | 'links': [{Link_}, ...] 310 | } 311 | 312 | Links: 313 | 314 | ================ ==================================================== 315 | Rel Links to 316 | ================ ==================================================== 317 | ``self`` This project_. 318 | ``build-list`` This project's `build list`_. 319 | ``latest-build`` URI for the redirect to this project's latest build. 320 | ``tag-list`` This project's `tag list`_. 321 | ================ ==================================================== 322 | 323 | Project list 324 | ~~~~~~~~~~~~ 325 | 326 | .. parsed-literal:: 327 | 328 | { 329 | 'projects': [{Project_}, ...], 330 | 'links': [{Link_}, ...] 331 | } 332 | 333 | Links: 334 | 335 | ======== ============ 336 | Rel Links to 337 | ======== ============ 338 | ``self`` This server. 339 | ======== ============ 340 | 341 | Tag 342 | ~~~ 343 | 344 | Tag detail. 345 | 346 | .. parsed-literal:: 347 | 348 | { 349 | 'tags': ['list', 'of', 'tags'], # Or just a single ['tag'] if this 350 | # is one tag. 351 | 352 | 'builds': [{Build_}, ...], 353 | 354 | 'count': 100, # total number of builds w/this tag 355 | 'num_pages': 4, # total number of pages 356 | 'page': 1 # current page number 357 | 'paginated': true # is this list paginated? 358 | 'per_page': 25, # number of builds per page 359 | 360 | 'links': [{Link_, ...}] 361 | } 362 | 363 | Links: 364 | 365 | ================ ====================================================== 366 | Rel Links to 367 | ================ ====================================================== 368 | ``self`` This `tag`_ (set) 369 | ``project`` The project_ in question. 370 | ``latest-build`` URI for the redirect to this project's latest build. 371 | ``next`` The next page of builds (if applicable). 372 | ``previous`` The previous page of builds (if applicable). 373 | ``first`` The first page of builds. 374 | ``last`` The last page of builds. 375 | =========== ====================================================== 376 | 377 | Tag list 378 | ~~~~~~~~ 379 | 380 | Representation: 381 | 382 | .. parsed-literal:: 383 | 384 | { 385 | 'tags': ['tag1', 'tag2', 'tag3'], 386 | 'links': [{Link_, ...}] 387 | } 388 | 389 | Links: 390 | 391 | =========== ====================================================== 392 | Rel Links to 393 | =========== ====================================================== 394 | ``self`` This `tag list`_ 395 | ``project`` The project_ in question. 396 | ``tag`` Each tag_ used by the project gets a link. 397 | =========== ====================================================== 398 | 399 | User 400 | ~~~~ 401 | 402 | Representation: 403 | 404 | .. parsed-literal:: 405 | 406 | { 407 | 'username': 'username', 408 | 'links': [{Link_}, ...] 409 | } 410 | 411 | Links: 412 | 413 | =========== ====================================================== 414 | Rel Links to 415 | =========== ====================================================== 416 | ``self`` This `user`_ 417 | ``builds`` `Build list`_ for this user. 418 | =========== ====================================================== 419 | 420 | 421 | User list 422 | ~~~~~~~~~ 423 | 424 | Representation: 425 | 426 | .. parsed-literal:: 427 | 428 | { 429 | 'users': [{User_}, ...], 430 | 431 | 'count': 100, # total number of users available 432 | 'num_pages': 4, # total number of pages 433 | 'page': 1 # current page number 434 | 'paginated': true # is this list paginated? 435 | 'per_page': 25, # number of users per page 436 | 'links': [{Link_, ...}] 437 | } 438 | 439 | Links: 440 | 441 | =========== ====================================================== 442 | Rel Links to 443 | =========== ====================================================== 444 | ``self`` This `user`_ 445 | =========== ====================================================== 446 | 447 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Devmason Server documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Mar 10 20:09:21 2010. 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.append(os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = [] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ['_templates'] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = '.rst' 32 | 33 | # The encoding of source files. 34 | #source_encoding = 'utf-8' 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # General information about the project. 40 | project = u'Devmason Server' 41 | copyright = u'2010, Eric Holscher & Jacob Kaplan-Moss' 42 | 43 | # The version info for the project you're documenting, acts as replacement for 44 | # |version| and |release|, also used in various other places throughout the 45 | # built documents. 46 | # 47 | # The short X.Y version. 48 | version = '1.0' 49 | # The full version, including alpha/beta/rc tags. 50 | release = '1.0' 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | #language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | #today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | #today_fmt = '%B %d, %Y' 61 | 62 | # List of documents that shouldn't be included in the build. 63 | #unused_docs = [] 64 | 65 | # List of directories, relative to source directory, that shouldn't be searched 66 | # for source files. 67 | exclude_trees = ['_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. Major themes that come with 93 | # Sphinx are currently 'default' and 'sphinxdoc'. 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_use_modindex = 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, an OpenSearch description file will be output, and all pages will 153 | # contain a tag referring to it. The value of this option must be the 154 | # base URL from which the finished HTML is served. 155 | #html_use_opensearch = '' 156 | 157 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 158 | #html_file_suffix = '' 159 | 160 | # Output file base name for HTML help builder. 161 | htmlhelp_basename = 'DevmasonServerdoc' 162 | 163 | 164 | # -- Options for LaTeX output -------------------------------------------------- 165 | 166 | # The paper size ('letter' or 'a4'). 167 | #latex_paper_size = 'letter' 168 | 169 | # The font size ('10pt', '11pt' or '12pt'). 170 | #latex_font_size = '10pt' 171 | 172 | # Grouping the document tree into LaTeX files. List of tuples 173 | # (source start file, target name, title, author, documentclass [howto/manual]). 174 | latex_documents = [ 175 | ('index', 'DevmasonServer.tex', u'Devmason Server Documentation', 176 | u'Eric Holscher \\& Jacob Kaplan-Moss', 'manual'), 177 | ] 178 | 179 | # The name of an image file (relative to this directory) to place at the top of 180 | # the title page. 181 | #latex_logo = None 182 | 183 | # For "manual" documents, if this is true, then toplevel headings are parts, 184 | # not chapters. 185 | #latex_use_parts = False 186 | 187 | # Additional stuff for the LaTeX preamble. 188 | #latex_preamble = '' 189 | 190 | # Documents to append as an appendix to all manuals. 191 | #latex_appendices = [] 192 | 193 | # If false, no module index is generated. 194 | #latex_use_modindex = True 195 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Devmason Server's documentation! 2 | =========================================== 3 | 4 | This is a server that is meant to be used for reporting test results of tests. 5 | Currently it's main focus is on Python, but there's no reason that it can't 6 | support other types of test results. 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :glob: 13 | 14 | * 15 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | How to install Devmason Server 2 | ============================== 3 | 4 | Devmason is easy to install. It comes with a setup.py, so you can easily 5 | install it:: 6 | 7 | virtualenv devmason 8 | cd devmason/ 9 | . bin/activate 10 | pip install -e git://github.com/ericholscher/devmason-server.git#egg=devmason-server 11 | cd src/devmason-server 12 | pip install -r pip_requirements.txt 13 | cd test_project 14 | ./manage.py syncdb --noinput 15 | ./manage.py loaddata devmason_server_test_data.json 16 | ./manage.py runserver 17 | 18 | 19 | That's all that it takes to get a running server up. Look at the test_project for examples on how to set up your urls and settings. 20 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Using the server is pretty simple. Most of the interaction is done through the API, which has a basic client library. The client library is located on github: http://github.com/ericholscher/devmason-utils/. 5 | 6 | Using the test runner 7 | --------------------- 8 | 9 | Once you have `devmason_utils` installed, it ships with it's own test runner that reports your test results to the server. Simply add the following in your settings:: 10 | 11 | TEST_RUNNER = 'devmason_utils.test_runner.run_tests' 12 | PB_USER = 'your_user' 13 | PB_PASS = 'your_pass' 14 | 15 | When you do this the username will be created for your on the server, then your results should automatically be sent to http://devmason.com. 16 | 17 | .. note:: 18 | 19 | A username and password is only required to create a project. If you're 20 | just sending results to someone else's project then you only need to set up 21 | your test runner. 22 | -------------------------------------------------------------------------------- /media/images/buttons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/buttons.gif -------------------------------------------------------------------------------- /media/images/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/fail.png -------------------------------------------------------------------------------- /media/images/field_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/field_bg.gif -------------------------------------------------------------------------------- /media/images/grade_boxers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/grade_boxers.gif -------------------------------------------------------------------------------- /media/images/grade_briefs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/grade_briefs.gif -------------------------------------------------------------------------------- /media/images/grade_cutoffs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/grade_cutoffs.gif -------------------------------------------------------------------------------- /media/images/grade_pants.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/grade_pants.gif -------------------------------------------------------------------------------- /media/images/hasselhoffian-recursion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/hasselhoffian-recursion.gif -------------------------------------------------------------------------------- /media/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/icon.png -------------------------------------------------------------------------------- /media/images/intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/intro.gif -------------------------------------------------------------------------------- /media/images/konami.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/konami.gif -------------------------------------------------------------------------------- /media/images/pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/pass.png -------------------------------------------------------------------------------- /media/images/rules/horz_ddd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/media/images/rules/horz_ddd.gif -------------------------------------------------------------------------------- /media/javascript/devmason.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | Tabs.init(); 3 | 4 | $('#header .search_form input').focus(function(e) { 5 | if (this.value == "Filter projects") { 6 | this.value = "" 7 | } 8 | }); 9 | $('#header .search_form input').blur(function(e) { 10 | if (this.value == "") { 11 | this.value = "Filter projects" 12 | } 13 | }); 14 | $('#header .search_form input').keyup(function(e) { 15 | project = $('#header .search_form input')[0].value; 16 | $('.site').each(function() { 17 | proj_name = this.children[0].textContent.trim(); 18 | if (proj_name.indexOf(project) == -1) { 19 | this.style.display = 'none'; 20 | } 21 | else { 22 | this.style.display = 'inline-block'; 23 | } 24 | }); 25 | /* e.preventDefault(); */ 26 | }); 27 | }); 28 | 29 | 30 | var Tabs = { 31 | init: function() { 32 | tabs = $('.tab_group .tabs li a'); 33 | 34 | // Hide all tab contents 35 | tabs_off = $('.tab_group .tabs li[class!=on] a'); 36 | tab_hrefs = tabs_off.map(function() { return $(this).attr('href'); }).get().join(', '); 37 | if (tab_hrefs) { 38 | $(tab_hrefs).hide(); 39 | } 40 | 41 | tabs.bind('click', this.click); 42 | }, 43 | 44 | click: function(e) { 45 | e.preventDefault(); 46 | target = $(this); 47 | tab_group = target.parents('.tab_group'); 48 | 49 | // Hide tab content areas 50 | tabs = tab_group.find('.tabs li a'); 51 | tabs.parent().removeClass('on') 52 | hrefs = tabs.map(function() { return $(this).attr('href'); }).get().join(', ') 53 | $(hrefs).hide(); 54 | 55 | // Show clicked tab 56 | target.parent().addClass('on'); 57 | $(target.attr('href')).show(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /media/javascript/jquery.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery JavaScript Library v1.3.2 3 | * http://jquery.com/ 4 | * 5 | * Copyright (c) 2009 John Resig 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://docs.jquery.com/License 8 | * 9 | * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) 10 | * Revision: 6246 11 | */ 12 | (function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!O.indexOf("",""]||(!O.indexOf("",""]||!O.indexOf("",""]||!o.support.htmlSerialize&&[1,"div
","
"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}}); 13 | /* 14 | * Sizzle CSS Selector Engine - v0.9.3 15 | * Copyright 2009, The Dojo Foundation 16 | * Released under the MIT, BSD, and GPL Licenses. 17 | * More information: http://sizzlejs.com/ 18 | */ 19 | (function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return UT[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="

";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="
";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
").append(M.responseText.replace(//g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='
';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})(); -------------------------------------------------------------------------------- /media/stylesheets/devmason.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | Project: pypants 4 | Constants: 5 | Greys: 6 | rgb(40, 40, 40) 7 | rgb(80, 80, 80) 8 | rgb(150, 150, 150) 9 | rgb(180, 180, 180) 10 | rgb(230, 230, 230) 11 | 12 | Blues: 13 | rgb(35, 150, 220) 14 | rgb(220, 230, 240) 15 | 16 | Greens: 17 | rgb(130, 190, 80) 18 | 19 | Orange: 20 | rgb(220, 90, 0) 21 | 22 | -------------------------------------------------------------- */ 23 | 24 | 25 | /* RESET 26 | -------------------------------------------------------------- */ 27 | html, body, div, span, object, iframe, 28 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 29 | a, abbr, acronym, address, big, cite, code, del, dfn, em, 30 | img, ins, kbd, q, samp, small, strike, strong, sub, sup, 31 | tt, var, dl, dt, dd, ol, ul, li, 32 | fieldset, form, label, legend, 33 | table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; vertical-align: baseline; background: transparent; } 34 | table { border-spacing: 0; border-collapse: collapse; } 35 | caption, th, td { text-align: left; font-weight: normal; } 36 | blockquote, q { quotes: none; } 37 | :focus { outline: none; } 38 | 39 | 40 | /* BASIC 41 | -------------------------------------------------------------- */ 42 | body { background: rgb(255, 255, 255); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.4em; color: rgb(40, 40, 40); } 43 | img { -ms-interpolation-mode: bicubic; } 44 | p.tags { font-size: 1.1em; color: rgb(160, 160, 160) !important; } 45 | p.tags a { color: rgb(150, 150, 150) !important; } 46 | p.tags a:hover { color: rgb(35, 150, 220) !important; } 47 | 48 | 49 | /* TYPE 50 | -------------------------------------------------------------- */ 51 | h1, h2, h3, h4, h5, h6 { margin-bottom: 0.25em; letter-spacing: -0.04em; line-height: 1.4em; } 52 | h1 { font-size: 2.0em; } 53 | h2 { font-size: 1.6em; } 54 | h3 { padding-bottom: 0.25em; font-size: 1.1em; border-bottom: 1px solid rgb(230, 230, 230); color: rgb(150, 150, 150); } 55 | 56 | a { color: rgb(35, 150, 220); text-decoration: none; } 57 | a:hover { text-decoration: underline; } 58 | 59 | p { margin: 0.5em 0 1em; font-size: 1.2em; line-height: 1.4em; } 60 | ul, ol { margin: 0.5em 0 1em; padding: 0 18px; } 61 | blockquote { margin: 0.5em 0 1em; padding-left: 10px; border-left: 1px solid rgb(230, 230, 230); } 62 | strong { font-weight: bold; } 63 | em { font-style: italic; } 64 | 65 | dl { margin: 0.5em 0 1em; } 66 | dt { font-weight: bold; } 67 | dd { margin: 0.5em 0; } 68 | 69 | table { margin: 0.5em 0 1em; width: 100%; font-size: 1em; } 70 | tr { border-bottom: 1px solid rgb(230, 230, 230); } 71 | tr:last-child { border: none; } 72 | th { padding: 10px 0; font-weight: bold; color: rgb(150, 150, 150); } 73 | td { padding: 10px 0; } 74 | tr.submit { border: none; } 75 | 76 | hr { border: none; border-bottom: 1px solid rgb(230, 230, 230); } 77 | 78 | /* FORMS 79 | -------------------------------------------------------------- */ 80 | label { vertical-align: top; } 81 | input, textarea, button { margin-top: -5px; padding: 5px; border: 1px solid rgb(180, 180, 180); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.1em; line-height: 1.3em; } 82 | input, textarea { background: rgb(255, 255, 255) url(../images/field_bg.gif) repeat-x; color: rgb(40, 40, 40); } 83 | input[type=file] { background: none; padding: 0; border: none; } 84 | input[type=hidden] { display: none; } 85 | button, .button { margin: 0; padding: 5px 25px; border: none; background: url(../images/buttons.gif) 0 -10px repeat-x; cursor: pointer; line-height: 1.3em; color: rgb(255, 255, 255); -webkit-border-radius: 4px; -moz-border-radius: 4px; -webkit-box-shadow: rgba(0, 0, 0, .3) 1px 1px 2px; } 86 | button:hover, .button:hover { background-position: 0 -100px; } 87 | button:active, .button:active { background-position: 0 -190px; -webkit-box-shadow: rgba(0, 0, 0, .4) 0px 0px 2px; } 88 | button::-moz-focus-inner { border: 0; } 89 | 90 | .button { padding: 7px 25px; } 91 | .button:hover { text-decoration: none; } 92 | 93 | fieldset { margin: 0.5em 0 1em; padding: 20px 20px 10px; border: 1px solid rgb(230, 230, 230); -webkit-border-radius: 6px; -moz-border-radius: 6px; } 94 | 95 | .change_form p { color: rgb(150, 150, 150); zoom: 1; } 96 | .change_form p:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 97 | .change_form p label { display: block; float: left; width: 13%; font-weight: bold; color: rgb(40, 40, 40); } 98 | .change_form .submit { margin-bottom: 0; text-align: right; } 99 | .change_form p .help_text { margin-top: 0px; margin-left: 13%; } 100 | .change_form .errors { color: #ff0000; } 101 | 102 | 103 | /* HEADER 104 | -------------------------------------------------------------- */ 105 | #header { position: relative; margin: 20px auto 0 auto; width: 90%; zoom: 1; } 106 | #header:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 107 | #header h1 { float: left; width: 30%; font-family: 'Myriad Pro', 'Helvetica Neue', helvetica, arial, sans-serif; } 108 | #header h1 a { margin: -5px; padding: 5px; color: #999; -webkit-border-radius: 4px; -moz-border-radius: 4px; } 109 | #header h1 p { font-size: 0.5em; color: rgb(150, 150, 150); } 110 | #header h1 a:hover { background: rgb(220, 230, 240); color: rgb(35, 150, 220); text-decoration: none; } 111 | #header .search_form { display: block; float: right; } 112 | #header .search_form input { padding: 5px; width: 225px; font-size: 0.9em; color: rgb(180, 180, 180); } 113 | #header .search_form input:focus { padding: 4px; color: rgb(40, 40, 40); border: 2px solid rgb(35, 150, 220); } 114 | #header .search_form button { padding: 6px 15px; font-size: 0.9em; } 115 | #header .search_form .all { display: block; font-size: 0.8em; } 116 | #header .search_form .all a { color: rgb(180, 180, 180); } 117 | #header .search_form .all a:hover { color: rgb(35, 150, 220); } 118 | 119 | /* BODY 120 | -------------------------------------------------------------- */ 121 | #body { position: relative; margin: 0 auto; width: 90%; zoom: 1; } 122 | #body:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 123 | .content { width: 1000; font-family: 'Myriad Pro', 'Helvetica Neue', helvetica, arial, sans-serif; } 124 | .content p { font-size: 0.9em; color:#969696; } 125 | 126 | .content_title { margin-bottom: 1em; border-bottom: 1px solid rgb(230, 230, 230); } 127 | .content_title h2 { font-size: 2.5em; } 128 | .content_title h2 a { color: rgb(40, 40, 40); } 129 | .content_title h2 em { font-size: 0.7em; font-weight: normal; font-style: normal; color: rgb(150, 150, 150); } 130 | .content_title h2 em a { color: rgb(150, 150, 150); } 131 | 132 | 133 | .aside { float: left; margin-left: 40px; margin-top: 73px; width: 159px; border-left: 1px solid rgb(230, 230, 230); } 134 | .aside h3 { padding: 0 0 0 10px; border: none; color: rgb(150, 150, 150); } 135 | .aside ul { list-style: none; margin: 0 0 1.5em 0; padding: 0; } 136 | .aside ul li a { display: block; margin: 0 -5px 1px 0; padding: 2px 10px; color: rgb(180, 180, 180); -webkit-border-top-right-radius: 11px; -webkit-border-bottom-right-radius: 11px; -moz-border-radius-topright: 11px; -moz-border-radius-bottomright: 11px; } 137 | .aside ul li a:hover { background: rgb(35, 150, 220); color: rgb(255, 255, 255); text-decoration: none; } 138 | .aside ul li.on a { background: rgb(220, 90, 0); color: rgb(255, 255, 255); } 139 | 140 | 141 | /* FOOTER 142 | -------------------------------------------------------------- */ 143 | #footer { position: relative; margin: 0 auto; width: 800px; zoom: 1; border-top: 1px solid rgb(230, 230, 230); } 144 | #footer:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 145 | #footer p { font-size: 0.9em; color: rgb(180, 180, 180); } 146 | 147 | 148 | /* ELEMENTS 149 | -------------------------------------------------------------- */ 150 | .project { margin-bottom: 2em; zoom: 1; } 151 | .project:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 152 | .project ul { margin-bottom: 0; } 153 | .project p { font-size: 1em; color: rgb(80, 80, 80); } 154 | .project .info { list-style: none; padding: 0; color: rgb(80, 80, 80); } 155 | 156 | .project .score { float: right; margin-left: 10px; width: 130px; text-align: center; } 157 | .project .score p { margin: 0; font-size: 1.1em; } 158 | .project .score img { margin: 0 0 5px 0; } 159 | .project .score .grade { display: block; font-size: 1.2em; font-weight: bold; color: rgb(80, 80, 80); } 160 | .project .score .points { display: block; font-size: 0.8em; color: rgb(150, 150, 150); } 161 | 162 | .report_card th.score { padding-right: 10px; text-align: right; } 163 | .report_card td { padding: 10px 0; } 164 | .report_card td.title { padding-left: 10px; } 165 | .report_card td.title h4 { margin: 0; font-size: 1.2em; font-weight: normal; } 166 | .report_card td.title .abbr { margin: 0 -4px; padding: 1px 5px; font-size: 0.65em; line-height: 1em; color: rgb(180, 180, 180); cursor: pointer; -webkit-border-radius: 3px; -moz-border-radius: 3px; } 167 | .report_card td.title .abbr:hover { background: rgb(220, 90, 0); color: rgb(255, 255, 255); } 168 | .report_card td.title p { margin: 0 0 0.3em 0; font-size: 1em; color: rgb(150, 150, 150); } 169 | .report_card td.score { padding-right: 10px; font-size: 1.3em; line-height: 1.3em; font-weight: bold; color: rgb(130, 190, 80); text-align: right; } 170 | .report_card td.score small { font-size: 0.8em; font-weight: normal; color: rgb(180, 180, 180); } 171 | 172 | .report_card tr.highlight td { background: #eee; } 173 | .report_card tr.highlight td.title h4 { font-size: 1.5em; font-weight: bold; } 174 | .report_card tr.highlight td.score { font-size: 1.5em; line-height: 1.3em; } 175 | 176 | .sparkline { float: left; height: 1em; margin: 0 0.5em; } 177 | .sparkline .index { position: relative; float: left; margin-right: 0; width: 1px; height: 100%; } 178 | .sparkline .index .count { display: block; position: absolute; bottom: 0; left: 0; width: 100%; height: 0; background: rgb(130, 190, 80); overflow: hidden; text-indent: -9999px; } 179 | .sparkline .on .count { background: rgb(220, 90, 0); } 180 | 181 | .recent_projects { list-style: none; margin-bottom: 30px; padding: 0; overflow: hidden; } 182 | .recent_projects li { display: block; float: left; width: 220px; } 183 | 184 | .report_history { margin-bottom: 2em; padding: 5px; background: #eee; -webkit-border-radius: 6px; -moz-border-radius: 6px; } 185 | .report_history .wrapper { padding: 10px; border: 1px solid #ddd; background: #fff; -webkit-border-radius: 4px; -moz-border-radius: 4px; } 186 | .report_history h3 { border: none; } 187 | 188 | .timeline { list-style: none; padding: 0; font-size: 0.75em; height: 6.5em; width: 53em; } 189 | .timeline li { position: relative; float: left; width: 10px; margin: 0 0.1em; height: 6em; } 190 | .timeline li a { display: block; height: 100%; color: rgb(150, 150, 150); } 191 | .timeline li .label { display: block; position: absolute; bottom: -2em; left: 0; background: rgb(255, 255, 255); width: 100%; height: 2em; line-height: 2em; text-align: center; } 192 | .timeline li a .count { display: block; position: absolute; bottom: 0; left: 0; height: 0; width: 100%; background: rgb(130, 190, 80); text-indent: -1000px; overflow: hidden; } 193 | .timeline li:hover { background: rgb(230, 230, 230); } 194 | .timeline li.on a .count { background: rgb(220, 90, 0); } 195 | .timeline li a:hover .count { background: rgb(35, 150, 220); text-decoration: none; } 196 | 197 | .evaluations { margin-bottom: 20px; } 198 | .evaluations td { padding: 4px 0; } 199 | 200 | .screencast { margin: 10px 0; padding: 10px; background: #d9ebc9; -webkit-border-radius: 6px; -moz-border-radius: 6px; } 201 | 202 | .pythong { position: absolute; top: 100px; left: 200px; padding: 10px; width: 220px; height: 340px; background: rgba(0, 0, 0, .5) url(../images/hasselhoffian-recursion.gif) 10px 10px no-repeat; -webkit-border-radius: 6px; -moz-border-radius: 6px; z-index: 999; } 203 | 204 | #intro { margin: 15px 0 30px 0; height: 155px; background: url(../images/intro.gif) no-repeat; border: none; overflow: hidden; text-indent: -1000px; } 205 | body.home .content { width: 440px; } 206 | body.home .aside { margin-top: 0; border: none; width: 319px; } 207 | .submit_project { margin-bottom: 20px; padding: 15px; background: #d9ebc9; -webkit-border-radius: 6px; -moz-border-radius: 6px; } 208 | .submit_project p { margin: 0; color: #717a68; } 209 | 210 | .add_project_form h3 { margin-bottom: 20px; padding-left: 0; font-size: 1.5em; color: #717a68; } 211 | .add_project_form fieldset { margin: 10px 0 0 0; padding: 0; border: none; } 212 | .add_project_form p { margin-bottom: 10px; } 213 | .add_project_form p label { padding-right: 10px; width: 90px; text-align: right; color: #717a68; } 214 | .add_project_form p input { width: 175px; } 215 | .add_project_form .submit { font-size: 1em; text-align: right; } 216 | 217 | .repo_form { margin-top: 10px; padding: 15px; background: #eee; } 218 | .repo_form p { margin: 0; font-size: 1.2em; } 219 | .repo_form p label { padding-top: 3px; width: 80px; } 220 | .repo_form p button { font-size: 0.8em; } 221 | .repo_form p input { width: 340px; font-size: 0.8em; } 222 | .repo_form p.help_text { margin: 2px 0 0 80px; font-size: .9em; } 223 | 224 | body.project_form .change_form p label { width: 100px; } 225 | body.project_form .change_form p input { width: 300px; } 226 | body.project_form .change_form p .errors { display: block; margin-left: 100px; } 227 | 228 | .importing { margin-bottom: 1em; padding: 10px; background: rgb(220, 90, 0); color: rgb(255, 255, 255); -webkit-border-radius: 6px; -moz-border-radius: 6px; } 229 | .importing p { margin: 0; text-align: center; } 230 | 231 | 232 | /* TABS 233 | -------------------------------------------------------------- */ 234 | .tabs { list-style: none; margin: 0; padding: 0; background: #fff url(../images/rules/horz_ddd.gif) left bottom repeat-x; font-size: 0.9em; overflow: hidden; } 235 | .tabs a { display: block; float: left; margin-right: 1px; padding: 5px 15px; background: rgb(240, 240, 240); color: rgb(100, 100, 100); -webkit-border-top-left-radius: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; } 236 | .tabs a:hover { background: rgb(35, 150, 220); color: rgb(255, 255, 255); text-decoration: none; } 237 | .tabs .on a, .tabs .on a:hover { background: rgb(255, 255, 255); border: 1px solid rgb(220, 220, 220); border-bottom: none; color: rgb(100, 100, 100); } 238 | 239 | 240 | body { padding: 5px; font-family: 'Helvetica-Neue', helvetica, arial, sans-serif; background: #eee; } 241 | a { text-decoration: none; } 242 | 243 | .site { display: inline-block; margin: 2px 2px 5px 2px; padding: 6px 2px; width: 247px; background: #fff; border: 1px solid #ddd; -webkit-border-radius: 6px; -moz-border-radius: 6px; -webkit-box-shadow: 1px 1px 3px #ddd; vertical-align: top; } 244 | .site h3 { margin: 0; padding: 3px 0; font-size: 14px; line-height: 20px; color: #333; text-align: center; } 245 | 246 | .sites p { margin: 0; font-size: 16px; color: #333; } 247 | .sites p a { display: block; width: 220px; margin: 0 5px; padding: 8px; border: 1px solid #ddd; border-bottom: none; background: #eee; color: #333; } 248 | .sites p a small { display: block; margin-top: 3px; font-size: 12px; color: #999; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } 249 | .sites p a strong { display: block; margin-bottom: 3px; font-size: 15px; color: #999; } 250 | .sites p a em { font-weight: bold; font-style: normal; } 251 | 252 | .sites p.first a { -webkit-border-top-left-radius: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; } 253 | .sites p.last a { border-bottom: 1px solid #c8de9d; -webkit-border-bottom-left-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomleft: 4px; -moz-border-radius-bottomright: 4px; } 254 | 255 | .sites p a:hover { background: #175e99 !important; border-color: #175e99 !important; color: #fff !important; } 256 | .sites p a:hover small, 257 | .sites p a:hover strong { color: #fff !important; } 258 | 259 | .sites p a.passed { background: #eeffcc; border-color: #c8de9d; } 260 | .sites p a.passed small { color: #616b4c; } 261 | .sites p a.passed strong { color: #98a978; } 262 | 263 | .sites p a.succeeded { background: #cc4949; border-color: #ad3e3e; } 264 | .sites p a.succeeded small, 265 | .sites p a.succeeded strong { color: #fff; } 266 | 267 | 268 | -------------------------------------------------------------------------------- /pip_requirements.txt: -------------------------------------------------------------------------------- 1 | -e hg+http://bitbucket.org/ubernostrum/django-registration#egg=registration 2 | -e git://github.com/ericholscher/devmason-server.git#egg=devmason-server 3 | -e git://github.com/ericholscher/devmason-utils.git#egg=devmason-utils 4 | 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='devmason_server', 4 | version='1.0pre-20091019', 5 | description='A Django implementation of a the pony-build server.', 6 | author = 'Eric Holscher, Jacob Kaplan-Moss', 7 | url = 'http://github.com/ericholscher/devmason-server', 8 | license = 'BSD', 9 | packages = ['devmason_server'], 10 | install_requires=['django-tagging>=0.3', 11 | 'django-piston>=0.2.2', 12 | 'django', 13 | 'mimeparse>=0.1.2'], 14 | ) 15 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericholscher/devmason-server/cb88f6a99e594c4202d37139fa73de238a4c35a0/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for paradigm project. 2 | import os 3 | PROJECT_DIR = os.path.dirname(__file__) 4 | TOP_DIR = os.path.join(PROJECT_DIR, '..') 5 | 6 | DEBUG = True 7 | #DEBUG = False 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = () 11 | 12 | MANAGERS = ADMINS 13 | 14 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | DATABASE_NAME = PROJECT_DIR + '/test_settings.db' # Or path to database file if using sqlite3. 16 | #mckenzie 17 | 18 | # Local time zone for this installation. Choices can be found here: 19 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 20 | # although not all choices may be available on all operating systems. 21 | # If running in a Windows environment this must be set to the same as your 22 | # system time zone. 23 | TIME_ZONE = 'America/Chicago' 24 | 25 | # Language code for this installation. All choices can be found here: 26 | # http://www.i18nguy.com/unicode/language-identifiers.html 27 | LANGUAGE_CODE = 'en-us' 28 | 29 | SITE_ID = 1 30 | 31 | # If you set this to False, Django will make some optimizations so as not 32 | # to load the internationalization machinery. 33 | USE_I18N = True 34 | 35 | # Absolute path to the directory that holds media. 36 | # Example: "/home/media/media.lawrence.com/" 37 | #MEDIA_ROOT = os.path.join(TOP_DIR, 'media') 38 | MEDIA_ROOT = '/Users/eric/checkouts/devmason-server/media/' 39 | 40 | 41 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 42 | # trailing slash if there is a path component (optional in other cases). 43 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 44 | MEDIA_URL = '/site_media/' 45 | 46 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 47 | # trailing slash. 48 | # Examples: "http://foo.com/media/", "/media/". 49 | ADMIN_MEDIA_PREFIX = '/admin_media/' 50 | 51 | # Make this unique, and don't share it with anybody. 52 | SECRET_KEY = '' 53 | 54 | # List of callables that know how to import templates from various sources. 55 | TEMPLATE_LOADERS = ( 56 | 'django.template.loaders.filesystem.load_template_source', 57 | 'django.template.loaders.app_directories.load_template_source', 58 | ) 59 | 60 | MIDDLEWARE_CLASSES = ( 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.contrib.sessions.middleware.SessionMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | ) 65 | 66 | ROOT_URLCONF = 'urls' 67 | 68 | TEMPLATE_DIRS = ( 69 | os.path.join(TOP_DIR, 'devmason_server/templates') 70 | ) 71 | 72 | INSTALLED_APPS = ( 73 | 'django.contrib.auth', 74 | 'django.contrib.contenttypes', 75 | 'django.contrib.comments', 76 | 'django.contrib.sessions', 77 | 'django.contrib.sites', 78 | 'django.contrib.admin', 79 | 'tagging', 80 | 'devmason_server', 81 | 'devmason_utils', 82 | ) 83 | 84 | 85 | TEST_RUNNER = 'devmason_utils.test_runner.run_tests' 86 | -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.conf import settings 3 | 4 | from django.contrib import admin 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | (r'^$', include('devmason_server.urls')), 9 | (r'devmason/', include('devmason_server.urls')), 10 | (r'^accounts/', include('registration.backends.default.urls')), 11 | (r'^admin/doc/', include('django.contrib.admindocs.urls')), 12 | url(r'^admin/(.*)', admin.site.root, name='admin-root'), 13 | 14 | ) 15 | if settings.DEBUG: 16 | urlpatterns += patterns('', 17 | (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}) 18 | ) 19 | 20 | 21 | --------------------------------------------------------------------------------