diff --git a/core/views.py b/core/views.py index 82426b243..3468005b8 100644 --- a/core/views.py +++ b/core/views.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.db.models import F from django.http import HttpResponse from django.shortcuts import render_to_response from django.template import RequestContext @@ -11,6 +12,7 @@ import os from projects.models import Project from projects.tasks import update_docs from projects.utils import find_file +from watching.models import PageView @csrf_view_exempt @@ -29,6 +31,11 @@ def serve_docs(request, username, project_slug, filename): filename = filename.rstrip('/') if not os.path.exists(os.path.join(proj.full_html_path, filename)): return HttpResponse("These docs haven't been built yet :(") + if 'html' in filename: + pageview, created = PageView.objects.get_or_create(project=proj, url=filename) + if not created: + pageview.count = F('count') + 1 + pageview.save() return serve(request, filename, proj.full_html_path) def render_header(request): diff --git a/media/css/core.css b/media/css/core.css index 57dc0a199..c9333bf6c 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -53,37 +53,16 @@ input[type="text"], input[type="password"] { width: 250px; height: 20px; margin- select { display: block; max-height: 300px; width: 250px; margin-bottom: 10px; font: 16/20px "inconsolata-1", "inconsolata-2", 'bitstream vera sans mono', 'andale mono', 'lucida console', monospace; } textarea { width: 435px; height: 150px; } input[type="submit"], input[type="button"], button { font-family: "ff-meta-web-pro-1", "ff-meta-web-pro-2", Arial, "Helvetica Neue", sans-serif; color: #666; font-weight: bold; padding: 4px 10px; border: none; background: #e6e6e6 url(../images/gradient.png) repeat-x bottom left; margin: 30px 5px 20px 0; text-shadow: 0 1px 0 rgba(255, 255, 255, 1); border: 1px solid #bfbfbf; } -input[type="submit"]:hover, input[type="button"]:hover, button:hover { background: #8ECC4C url(../images/gradient.png) repeat-x bottom left; color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); } +input[type="submit"]:hover, input[type="button"]:hover, button:hover { background-color: #8ECC4C; color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); border-color: #8ECC4C; } fieldset { border: 1px solid #bfbfbf; padding: 15px; -moz-border-radius: 3px; -webkit-border-radius: 3px; margin-bottom: 15px; } input[type="hidden"] { display: none; } input[type="checkbox"], input[type="radio"] { display: inline; } label { display: block; margin-bottom: 4px; font-weight: bold; } -/* header */ - -#rtfd-header { height: 50px; min-width: 780px; background: #465158; overflow: hidden; text-align: left; border-bottom: 1px solid #ccc; } - - /* header title */ - .rtfd-header-title h1 { font-size: 20px; padding: 5px 20px; color: #fff; } - - /* header search */ - .rtfd-header-search { position: absolute; top: 10px; left: 230px; } - .rtfd-header-search input { padding: 0 5px; margin: 0; height: 25px; font-size: 14px; float: left; -moz-border-radius: 0; -webkit-border-radius: 0; border: none; } - .rtfd-header-search input[type="text"] { -moz-border-radius-topleft: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-top-left-radius: 3px; -webkit-border-bottom-left-radius: 3px; } - .rtfd-header-search input[type="submit"] { -moz-border-radius-topright: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-top-right-radius: 3px; -webkit-border-bottom-right-radius: 3px; padding: 0 12px; } - - /* header nav */ - .rtfd-header-nav { position: absolute; top: 0; right: 10px; } - .rtfd-header-nav ul li { float: left; } - .rtfd-header-nav ul li a { display: block; text-decoration: none; background: url(../images/gradient.png) bottom left repeat-x #697983; padding: 15px; color: #fff; } - .rtfd-header-nav ul li a:hover { background-color: #8CA1AF; } - .rtfd-header-nav ul li.active a, .header-nav ul li.active a:hover { background-color: #BAC7CF; } - - /* content */ -#content { margin-top: 80px; } +#content { padding-top: 80px; } /* module */ @@ -127,6 +106,13 @@ label { display: block; margin-bottom: 4px; font-weight: bold; } .pagination .current.page, .pagination .current.page:hover { color: #444; background: url("../images/gradient-light.png") repeat-x scroll left bottom #d9d9d9; } +/* footer */ + +#footer { margin: 100px 0; color: #aaa; } +#footer a { color: #aaa; } +#footer a:hover { color: #666; } + + /* utils */ .clear { clear: both; } diff --git a/media/css/header.css b/media/css/header.css new file mode 100644 index 000000000..683312a0b --- /dev/null +++ b/media/css/header.css @@ -0,0 +1,24 @@ + +body { padding-top: 50px; } + +/* header */ + +#rtfd-header { position: absolute; top: 0; left: 0; width: 100%; font: 16px/20px "ff-meta-web-pro-1","ff-meta-web-pro-2", Arial, "Helvetica Neue", sans-serif; height: 50px; min-width: 780px; background: #465158; overflow: hidden; text-align: left; } +#rtfd-header ul { margin: 0; padding: 0; list-style: none; } + + /* header title */ + .rtfd-header-title h1 { font-size: 20px; padding: 5px 20px; color: #fff; } + + /* header search */ + .rtfd-header-search { position: absolute; top: 10px; left: 230px; } + .rtfd-header-search input { padding: 0 5px; margin: 0; height: 25px; font-size: 14px; float: left; -moz-border-radius: 0; -webkit-border-radius: 0; border: none; } + .rtfd-header-search input[type="text"] { -moz-border-radius-topleft: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-top-left-radius: 3px; -webkit-border-bottom-left-radius: 3px; width: 220px; } + .rtfd-header-search input[type="submit"] { font-family: "ff-meta-web-pro-1", "ff-meta-web-pro-2", Arial, "Helvetica Neue", sans-serif; -moz-border-radius-topright: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-top-right-radius: 3px; -webkit-border-bottom-right-radius: 3px; padding: 0 12px; background: #e6e6e6 url(../images/gradient.png) repeat-x bottom left; font-weight: bold; color: #666; } + .rtfd-header-search input[type="submit"]:hover { text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); background-color: #8ECC4C; color: #fff; } + + /* header nav */ + .rtfd-header-nav { position: absolute; top: 0; right: 10px; } + .rtfd-header-nav ul li { float: left; } + .rtfd-header-nav ul li a { display: block; text-decoration: none; background: url(../images/gradient.png) bottom left repeat-x #697983; padding: 15px; color: #fff; } + .rtfd-header-nav ul li a:hover { background-color: #8CA1AF; } + .rtfd-header-nav ul li.active a, .header-nav ul li.active a:hover { background-color: #BAC7CF; } diff --git a/pip_requirements.txt b/pip_requirements.txt index 26b16a5df..49b707693 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -15,3 +15,4 @@ django-extensions -e git+http://github.com/ericflo/django-pagination.git#egg=pagination -e hg+http://bitbucket.org/ubernostrum/django-profiles/#egg=profiles -e git+http://github.com/nathanborror/django-registration.git#egg=django-registration +-e git+http://github.com/nathanborror/django-basic-apps.git#egg=django-basic-apps diff --git a/projects/models.py b/projects/models.py index cc8e06bea..a8461d0f5 100644 --- a/projects/models.py +++ b/projects/models.py @@ -36,7 +36,7 @@ class Project(models.Model): return self.name def get_absolute_url(self): - return reverse('projects_detail', args=[self.user.username, self.slug, '']) + return reverse('projects_detail', args=[self.user.username, self.slug]) def user_doc_path(self): return os.path.join(settings.DOCROOT, self.user.username, self.slug) @@ -182,7 +182,6 @@ class File(models.Model): def filename(self): return os.path.join( self.project.conf.path, - self.project.slug, '%s.rst' % self.denormalized_path ) diff --git a/projects/templates/projects/index.rst.html b/projects/templates/projects/index.rst.html index 532ff87b3..2bf5e02fe 100644 --- a/projects/templates/projects/index.rst.html +++ b/projects/templates/projects/index.rst.html @@ -6,7 +6,8 @@ Contents: .. toctree:: - :maxdepth: 2 - :glob: - {{ project.slug }}/* + {% load projects_tags %} + {% for file in project|annotated_tree %} + {{ file.denormalized_path }} + {% endfor %} diff --git a/projects/templatetags/__init__.py b/projects/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/projects/templatetags/projects_tags.py b/projects/templatetags/projects_tags.py new file mode 100644 index 000000000..16850b43e --- /dev/null +++ b/projects/templatetags/projects_tags.py @@ -0,0 +1,19 @@ +from django import template + +register = template.Library() + +@register.filter +def top_level_files(project): + return project.files.filter(parent__isnull=True) + +@register.filter +def annotated_tree(project, max_depth=99): + annotated = [] + def walk_tree(qs, depth=1): + for obj in qs: + obj.depth = depth + annotated.append(obj) + if depth < max_depth: + walk_tree(obj.children.order_by('ordering'), depth+1) + walk_tree(project.files.filter(parent__isnull=True).order_by('ordering')) + return annotated diff --git a/projects/urls/public.py b/projects/urls/public.py index d21019b4b..b01853b14 100644 --- a/projects/urls/public.py +++ b/projects/urls/public.py @@ -17,6 +17,10 @@ urlpatterns = patterns('projects.views.public', 'project_index', name='project_tag_detail', ), + url(r'^(?P\w+)/(?P[-\w]+)/$', + 'project_detail', + name='projects_detail' + ), url(r'^(?P\w+)/$', 'project_index', name='projects_user_list' diff --git a/settings/base.py b/settings/base.py index 18c995263..2d38eac14 100644 --- a/settings/base.py +++ b/settings/base.py @@ -85,10 +85,12 @@ INSTALLED_APPS = ( 'south', 'taggit', 'django_extensions', + 'basic.flagging', # our apps 'projects', 'core', + 'watching', ) @@ -99,4 +101,3 @@ EMAIL_USE_TLS = True EMAIL_HOST = 'golem' EMAIL_HOST_USER = 'no-reply@readthedocs.com' EMAIL_PORT = 25 - diff --git a/templates/base.html b/templates/base.html index 5da2d34dc..f0f2867b3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,6 +11,7 @@ + @@ -43,6 +44,20 @@ + + + + diff --git a/urls.py b/urls.py index b4ab5e867..fc74d2524 100644 --- a/urls.py +++ b/urls.py @@ -7,9 +7,9 @@ admin.autodiscover() urlpatterns = patterns('', url(r'^accounts/', include('registration.backends.default.urls')), url(r'^dashboard/', include('projects.urls.private')), - url(r'^projects/(?P\w+)/(?P[-\w]+)/(?P.*)$', + url(r'^projects/(?P\w+)/(?P[-\w]+)/docs/(?P.*)$', 'core.views.serve_docs', - name='projects_detail' + name='docs_detail' ), url(r'render_header/', 'core.views.render_header', diff --git a/watching/__init__.py b/watching/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/watching/admin.py b/watching/admin.py new file mode 100644 index 000000000..ec149109f --- /dev/null +++ b/watching/admin.py @@ -0,0 +1,4 @@ +from watching.models import PageView +from django.contrib import admin + +admin.site.register(PageView) diff --git a/watching/models.py b/watching/models.py new file mode 100644 index 000000000..e4c481df9 --- /dev/null +++ b/watching/models.py @@ -0,0 +1,11 @@ +from django.db import models +from projects.models import Project + +class PageView(models.Model): + project = models.ForeignKey(Project, related_name='page_views') + url = models.CharField(max_length=255) + count = models.IntegerField(default=1) + + + def __unicode__(self): + return u"Page views for %s's url %s" % (self.project, self.url) diff --git a/watching/tests.py b/watching/tests.py new file mode 100644 index 000000000..87fea7dcc --- /dev/null +++ b/watching/tests.py @@ -0,0 +1,67 @@ +from django.conf import settings +from django.test import TestCase +from django.core.urlresolvers import reverse + +import json + +from projects.models import Conf + +data = """ +{ + "before": "5aef35982fb2d34e9d9d4502f6ede1072793222d", + "repository": { + "url": "http://github.com/beetletweezers/tweezers", + "name": "github", + "description": "You're lookin' at it.", + "watchers": 5, + "forks": 2, + "private": 1, + "owner": { + "email": "chris@ozmm.org", + "name": "defunkt" + } + }, + "commits": [ + { + "id": "41a212ee83ca127e3c8cf465891ab7216a705f59", + "url": "http://github.com/defunkt/github/commit/41a212ee83ca127e3c8cf465891ab7216a705f59", + "author": { + "email": "chris@ozmm.org", + "name": "Chris Wanstrath" + }, + "message": "okay i give in", + "timestamp": "2008-02-15T14:57:17-08:00", + "added": ["filepath.rb"] + }, + { + "id": "de8251ff97ee194a289832576287d6f8ad74e3d0", + "url": "http://github.com/defunkt/github/commit/de8251ff97ee194a289832576287d6f8ad74e3d0", + "author": { + "email": "chris@ozmm.org", + "name": "Chris Wanstrath" + }, + "message": "update pricing a tad", + "timestamp": "2008-02-15T14:36:34-08:00" + } + ], + "after": "de8251ff97ee194a289832576287d6f8ad74e3d0", + "ref": "refs/heads/master" +} +""" + +class Basic(TestCase): + fixtures=['eric', 'test_data'] + + def setUp(self): + settings.CELERY_ALWAYS_EAGER = True + + def tearDown(self): + settings.CELERY_ALWAYS_EAGER = False + + def test_github(self): + resp = self.client.post('/github', {'payload': data}) + self.assertEqual(Conf.objects.count(), 1) + conf = Conf.objects.all()[0] + self.assertEqual(conf.theme, 'default') + self.assertTrue(conf.path is not None) + diff --git a/watching/urls.py b/watching/urls.py new file mode 100644 index 000000000..983cf2ede --- /dev/null +++ b/watching/urls.py @@ -0,0 +1,28 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('watching.views', + url(r'^$', + 'project_index', + name='projects_list' + ), + url(r'^tags/$', + 'tag_index', + name='project_tag_list', + ), + url(r'^search/', + 'search', + name='search', + ), + url(r'^tags/(?P\w+)/$', + 'project_index', + name='project_tag_detail', + ), + url(r'^projects/(?P\w+)/(?P[-\w]+)/$', + 'project_detail', + name='projects_detail' + ), + url(r'^(?P\w+)/$', + 'project_index', + name='projects_user_list' + ), +) diff --git a/watching/views.py b/watching/views.py new file mode 100644 index 000000000..dbbedb7d5 --- /dev/null +++ b/watching/views.py @@ -0,0 +1,33 @@ +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.views.decorators.csrf import csrf_view_exempt +from django.views.static import serve + +import json + +from projects.models import Project +from projects.tasks import update_docs +from projects.utils import find_file + + +@csrf_view_exempt +def github_build(request): + obj = json.loads(request.POST['payload']) + name = obj['repository']['name'] + url = obj['repository']['url'] + project = Project.objects.get(repo=url) + update_docs.delay(pk=project.pk) + return HttpResponse('Build Started') + +def serve_docs(request, username, project_slug, filename): + proj = Project.objects.get(slug=project_slug, user__username=username) + if not filename: + filename = "index.html" + filename = filename.rstrip('/') + return serve(request, filename, proj.full_html_path) + +def render_header(request): + return render_to_response('core/header.html', {}, + context_instance=RequestContext(request))