nautilebleu / django_hg

django_hg allows managing (create, authenticate, clone/push/pull…) Mercurial repositories throught django.

Clone this repository (size: 144.8 KB): HTTPS / SSH
$ hg clone http://bitbucket.org/nautilebleu/django_hg/
commit 31: e258ed471d54
parent 30: 8d6bb0d91835
branch: default
Add a small search engine for repositories list, add doctest to templatetags and manage tz
Goulwen Reboux / nautilebleu
9 months ago

Changed (Δ7.6 KB):

raw changeset »

docs/build/index.html (48 lines added, 8 lines removed)

docs/source/index.txt (48 lines added, 7 lines removed)

forms.py (30 lines added, 2 lines removed)

models.py (30 lines added, 9 lines removed)

site_media/css/django_hg.css (0 lines added, 10 lines removed)

templates/django_hg/changeset_info.html (1 lines added, 1 lines removed)

templates/django_hg/list.html (16 lines added, 11 lines removed)

templatetags/django_hg_tags.py (62 lines added, 3 lines removed)

views.py (17 lines added, 2 lines removed)

Up to file-list docs/build/index.html:

21
21
<li><a href="#install">Install</a></li>
22
22
<li><a href="#usage">Usage</a><ul>
23
23
<li><a href="#administration">Administration</a></li>
24
<li><a href="#public">Public</a></li>
24
<li><a href="#public">Public</a><ul>
25
<li><a href="#list">List</a></li>
26
<li><a href="#repository">Repository</a></li>
27
<li><a href="#file">File</a></li>
28
</ul>
29
</li>
25
30
<li><a href="#hg-commands-clone-push-and-pull">Hg commands (clone, push and pull)</a></li>
26
31
</ul>
27
32
</li>
33
<li><a href="#templating">Templating</a></li>
28
34
<li><a href="#deployment">Deployment</a></li>
29
35
<li><a href="#to-dos">To-dos</a></li>
30
36
<li><a href="#requirements">Requirements</a></li>
@@ -69,6 +75,10 @@ and install is quite easy:</p>
69
75
  <span class="s">"private"</span><span class="p">:</span> <span class="s">"/Users/goulwen/Repositories/private/"</span>
70
76
<span class="p">}</span>
71
77
<span class="n">DJANGO_HG_PAGER_ITEMS</span> <span class="o">=</span> <span class="mf">30</span>
78
<span class="c"># one of pygment styles : autumn, borland, bw, colorful, default, emacs,</span>
79
<span class="c"># friendly, fruity, manni, murphy, native, pastie, perldoc, tango, trac, vim,</span>
80
<span class="c"># vs</span>
81
<span class="n">DJANGO_HG_PYGMENT_STYLE</span> <span class="o">=</span> <span class="s">'tango'</span>
72
82
73
83
<span class="c"># templates dir</span>
74
84
<span class="n">TEMPLATE_DIRS</span> <span class="o">=</span> <span class="p">(</span>
@@ -113,6 +123,7 @@ roles <code>Read</code>, <code>Read/Writ
113
123
repositories in browsers and when handling commands like <code>clone</code>, <code>push</code> and
114
124
<code>pull</code>.</p>
115
125
<h3 id="public">Public</h3>
126
<h4 id="list">List</h4>
116
127
<p>django_hg lets you browse a list of repositories, depending of the user
117
128
permissions:</p>
118
129
<ul>
@@ -120,15 +131,42 @@ permissions:</p>
120
131
<li>If the user is authenticated, private repositories where the user has role are
121
132
  displayed, plus public ones.</li>
122
133
</ul>
123
<p>The repository view displays informations about the latest revision (aka <code>tip</code>
124
in Mercurial) at the top of the page and below, the top level of the repository.
125
You can browse the repository or display the changelog.</p>
126
<p>Once in the changelog, you can browse the repository manifest at this given
127
revision.</p>
128
<p>When browsing the repository, you can view the filelog of a given file.</p>
134
<p>A search facility is available. Currently, search looks in repositories' title
135
and summary, but not in the source code or the changelog.</p>
136
<p>Results can also be filtered using current user permissions, so only
137
repositories he owned are displayed, for example.</p>
138
<h4 id="repository">Repository</h4>
139
<p>When accessing a repository, you can display 3 differents views:</p>
140
<ul>
141
<li>The <code>overview</code> view summarizes the repository, showing the latest changeset
142
  (aka <code>tip</code> in Mercurial, people involved in the project. This page will evolve
143
  in the next weeks to be raffined.</li>
144
<li>The <code>browse</code> view displays the repository at a given revision (<code>tip</code> by default)</li>
145
<li>The <code>changesets</code>view diplays the list of revisions of the repository.
146
  You can browse the repository at a given revision or display the changelog
147
  details.</li>
148
</ul>
149
<h4 id="file">File</h4>
150
<p>From the <code>browse</code> and the <code>changeset</code> views, repository files can be accessed:</p>
151
<ul>
152
<li>The <code>log</code> view displays changes history of the file.</li>
153
<li>The <code>view</code> view shows the file, depending of its mimetype:</li>
154
<li>If a <a href="http://pygments.org">Pygments [en]</a> lexer can be found, the file is displayed
155
    with syntax coloration</li>
156
<li>If the mimetype corresponds to a picture (PNG, GIF, JPEG) or a PDF, the file
157
    is displayed</li>
158
<li>Otherwise, you can download it or force the display a plain text.</li>
159
<li>The <code>diff</code> view will allow the comparison between two revisions, but is far
160
  from being operational yet.</li>
161
</ul>
129
162
<h3 id="hg-commands-clone-push-and-pull">Hg commands (clone, push and pull)</h3>
130
163
<p>django_hg supports <code>clone</code>, <code>push</code> and <code>pull</code> over HTTP throught django,
131
164
including authentication.</p>
165
<h2 id="templating">Templating</h2>
166
<p>django_hg tries to follow
167
<a href="http://ericholscher.com/projects/django-conventions/app/">django reusable apps conventions [en]</a> in naming of
168
blocks.</p>
169
<p>A simple CSS is provided in the <code>site_media</code> directory. Feel free to</p>
132
170
<h2 id="deployment">Deployment</h2>
133
171
<p>Because django_hg relies on wsgi objects to performs Mercurial commands, django
134
172
must be deployed throught <a href="http://docs.djangoproject.com/en/1.0/howto/deployment/modwsgi/">mod_wsgi [en]</a>.</p>
@@ -140,7 +178,8 @@ order to <a href="http://www.arnebrodows
140
178
141
179
<h2 id="to-dos">To-dos</h2>
142
180
<ul>
143
<li>Add missing features such as search in changesets, filelog, diff</li>
181
<li>Add missing features such as search in list, changesets, filelog</li>
182
<li><code>diff</code> and <code>archive</code></li>
144
183
<li>Add Ajax browsing for the repository</li>
145
184
</ul>
146
185
<h2 id="requirements">Requirements</h2>
@@ -148,6 +187,7 @@ order to <a href="http://www.arnebrodows
148
187
<li><a href="http://python.org/">Python 2.5+ [en]</a></li>
149
188
<li><a href="http://selenic.com/mercurial/">Mercurial 1.2 [en]</a></li>
150
189
<li><a href="http://djangoproject.com/">django 1.0.2 [en]</a></li>
190
<li><a href="http://pygments.org">Pygments [en]</a></li>
151
191
</ul>
152
192
<p>For deployement:</p>
153
193
<ul>

Up to file-list docs/source/index.txt:

@@ -46,6 +46,10 @@ Configure your `settings.py`:
46
46
      "private": "/Users/goulwen/Repositories/private/"
47
47
    }
48
48
    DJANGO_HG_PAGER_ITEMS = 30
49
    # one of pygment styles : autumn, borland, bw, colorful, default, emacs,
50
    # friendly, fruity, manni, murphy, native, pastie, perldoc, tango, trac, vim,
51
    # vs
52
    DJANGO_HG_PYGMENT_STYLE = 'tango'
49
53
50
54
    # templates dir
51
55
    TEMPLATE_DIRS = (
@@ -98,6 +102,8 @@ repositories in browsers and when handli
98
102
99
103
### Public
100
104
105
#### List
106
101
107
django_hg lets you browse a list of repositories, depending of the user
102
108
permissions:
103
109
@@ -105,20 +111,52 @@ permissions:
105
111
* If the user is authenticated, private repositories where the user has role are
106
112
  displayed, plus public ones.
107
113
108
The repository view displays informations about the latest revision (aka `tip`
109
in Mercurial) at the top of the page and below, the top level of the repository.
110
You can browse the repository or display the changelog.
114
A search facility is available. Currently, search looks in repositories' title
115
and summary, but not in the source code or the changelog.
111
116
112
Once in the changelog, you can browse the repository manifest at this given
113
revision.
117
Results can also be filtered using current user permissions, so only
118
repositories he owned are displayed, for example.
114
119
115
When browsing the repository, you can view the filelog of a given file.
120
#### Repository
121
122
When accessing a repository, you can display 3 differents views:
123
124
* The `overview` view summarizes the repository, showing the latest changeset
125
  (aka `tip` in Mercurial, people involved in the project. This page will evolve
126
  in the next weeks to be raffined.
127
* The `browse` view displays the repository at a given revision (`tip` by default)
128
* The `changesets`view diplays the list of revisions of the repository.
129
  You can browse the repository at a given revision or display the changelog
130
  details.
131
132
#### File
133
134
From the `browse` and the `changeset` views, repository files can be accessed:
135
136
* The `log` view displays changes history of the file.
137
* The `view` view shows the file, depending of its mimetype:
138
  * If a [Pygments [en]][_pygments] lexer can be found, the file is displayed
139
    with syntax coloration
140
  * If the mimetype corresponds to a picture (PNG, GIF, JPEG) or a PDF, the file
141
    is displayed
142
  * Otherwise, you can download it or force the display a plain text.
143
* The `diff` view will allow the comparison between two revisions, but is far
144
  from being operational yet.
116
145
117
146
### Hg commands (clone, push and pull)
118
147
119
148
django_hg supports `clone`, `push` and `pull` over HTTP throught django,
120
149
including authentication.
121
150
151
## Templating
152
153
django_hg tries to follow
154
[django reusable apps conventions [en]][_django_reusable_apps] in naming of
155
blocks.
156
157
A simple CSS is provided in the `site_media` directory. Feel free to
158
159
122
160
## Deployment
123
161
124
162
Because django_hg relies on wsgi objects to performs Mercurial commands, django
@@ -132,7 +170,8 @@ order to [pass authentication [en]][_wsg
132
170
133
171
## To-dos
134
172
135
* Add missing features such as search in changesets, filelog, diff
173
* Add missing features such as search in list, changesets, filelog
174
* `diff` and `archive`
136
175
* Add Ajax browsing for the repository
137
176
138
177
## Requirements
@@ -140,6 +179,7 @@ order to [pass authentication [en]][_wsg
140
179
* [Python 2.5+ [en]][_python]
141
180
* [Mercurial 1.2 [en]][_mercurial]
142
181
* [django 1.0.2 [en]][_django]
182
* [Pygments [en]][_pygments]
143
183
144
184
For deployement:
145
185
@@ -162,5 +202,6 @@ For deployement:
162
202
[_mysql]: http://www.mysql.com/
163
203
[_postgresql]: http://www.postgresql.org/
164
204
[_python]: http://python.org/
205
[_pygments]: http://pygments.org
165
206
[_mod_wsgi]: http://code.google.com/p/modwsgi/
166
207
[_wsgipassauthorization]: http://www.arnebrodowski.de/blog/508-Django,-mod_wsgi-and-HTTP-Authentication.html

Up to file-list forms.py:

1
1
# coding=utf-8
2
#!/usr/bin/env python
3
2
from django import forms
4
from django_hg.models import HgRepository
3
#from django_hg.models import HgRepository
4
5
DISPLAY_CHOICES = (
6
    ('all', 'All'),
7
    ('only_member', 'Only repositories I contribute'),
8
    ('only_owner', 'Only repositories I own')
9
)
10
11
class HgRepositoryForm(forms.Form):
12
    search = forms.CharField(max_length=100, required=False)
13
    display = forms.CharField(max_length=11,
14
                              required=False,
15
                              widget= forms.Select(choices=DISPLAY_CHOICES))
16
17
    def clean_display(self):
18
        data = self.data['display']
19
        return data
20
21
    def clean_search(self):
22
        data = self.data['search']
23
        #print 'dans clean_name' + str(data)
24
25
        return data
26
27
    def is_valid(self):
28
        #print 'avant'
29
        #print self['search'].data
30
        #self.clean()
31
        #print 'apres'
32
        return True

Up to file-list models.py:

3
3
from mercurial import ui, hg
4
4
from django.db import models
5
5
from django.contrib.auth.models import User
6
from django.db.models import Q
6
7
from django.utils.translation import ugettext_lazy as _
7
8
from django.core.urlresolvers import reverse
8
9
from django.conf import settings as global_settings
@@ -28,8 +29,10 @@ class HgFile():
28
29
    def __init__(self, name, path, hg_view):
29
30
        self.name = name
30
31
        self.path = path + name
32
31
33
        if self.name.find('/')>-1:
32
34
            self.is_dir = True
35
            #self.size = self.size
33
36
        else:
34
37
            self.is_dir = False
35
38
            fctx = hg_view.ctx.filectx(self.path)
@@ -85,6 +88,10 @@ class HgContext():
85
88
                names.append(remain)
86
89
            else:
87
90
                d = remain.split('/')
91
                #print d[len(d)-1]
92
                #fctx = self.ctx.filectx(remain)
93
                #print fctx.size()
94
88
95
                if (d[0] not in dirs) :
89
96
                    dirs.append(d[0])
90
97
                    names.append(d[0] + '/')
@@ -102,34 +109,48 @@ class HgContext():
102
109
103
110
104
111
class HgRepositoryManager(models.Manager):
105
    def filter_for_user(self, user, permission):
112
    def filter_for_user(self, user, permission, **kwargs):
113
        qs = HgRepository.objects
114
        # search filtering
115
        for arg in kwargs:
116
            if arg == 'search' and kwargs[arg] != '' and kwargs[arg] is not None:
117
                for keyword in kwargs[arg].split(' '):
118
                    qs = qs.filter(Q(name__icontains=keyword) | Q(summary__icontains=keyword))
119
            if arg == 'display' and kwargs['display'] != 'all':
120
                if kwargs[arg] == 'only_member':
121
                    qs = qs.filter(repositoryuser__user__id = user.id,
122
                                   repositoryuser__permission__gte=1)
123
                elif kwargs[arg] == 'only_owner':
124
                    qs = qs.filter(repositoryuser__user__id = user.id,
125
                                   repositoryuser__permission=7)
126
106
127
        if user.is_authenticated():
107
            return HgRepository.objects.distinct().extra(
108
                where=['anonymous_access=True or '
128
            return qs.distinct().extra(
129
                where=['(anonymous_access=True or '
109
130
                     + '(anonymous_access=False '
110
131
                     + 'and django_hg_repositoryuser.user_id=%s '
111
132
                     + 'and django_hg_repositoryuser.source_repository_id=django_hg_hgrepository.id '
112
                     + 'and django_hg_repositoryuser.permission>=%s)'],
133
                     + 'and django_hg_repositoryuser.permission>=%s))'],
113
134
                params= [user.id, permission],
114
135
                tables=['django_hg_repositoryuser']
115
136
                )
116
137
        else:
117
            return HgRepository.objects.filter(anonymous_access=True)
138
            return qs.filter(anonymous_access=True)
118
139
119
    def count_for_user(self, user, permission=3):
140
    def count_for_user(self, user, permission=3, **kwargs):
120
141
        """
121
142
        Return the total number of source repositories which the user has at
122
143
        least the given permission level
123
144
        """
124
        return self.filter_for_user(user, permission).count()
145
        return self.filter_for_user(user, permission, **kwargs).count()
125
146
126
147
127
    def get_for_user(self, user, permission=3):
148
    def get_for_user(self, user, permission=3, **kwargs):
128
149
        """
129
150
        Return a list of source repositories which the user has at least the
130
151
        given permission level
131
152
        """
132
        return self.filter_for_user(user, permission).order_by('name')
153
        return self.filter_for_user(user, permission, **kwargs).order_by('name')
133
154
134
155
135
156
class HgRepository(models.Model):

Up to file-list site_media/css/django_hg.css:

@@ -52,16 +52,6 @@ div.diff {
52
52
    background: #FFDDDD;
53
53
  }
54
54
55
.django_hg_toolbar {
56
  float: right ;
57
  padding: 0px 5px ;
58
  width: 360px ;
59
}
60
61
  .django_hg_toolbar dl dd {
62
    margin: 0px
63
  }
64
65
55
.file {
66
56
  display: inline-block ;
67
57
}

Up to file-list templates/django_hg/changeset_info.html:

2
2
{% load django_hg_tags %}
3
3
4
4
<dl>
5
    {% blocktrans with ctx.date|format_hg_date as date and ctx.user as user %}
5
    {% blocktrans with ctx.date|format_hg_date as date and ctx.user|format_hg_user as user %}
6
6
    <dt>On {{ date }}, {{ user }} has committed
7
7
    {% endblocktrans %}
8
8
        <a href="{% url hg-changeset repo ctx.rev  %}">

Up to file-list templates/django_hg/list.html:

33
33
    <div class="repo_views"></div>
34
34
35
35
    <div id="django_hg_container">
36
        <div class="django_hg_toolbar">
36
        <div id="repo_bar">
37
           <form action="{% url hg-list %}" method="get">
37
38
            <ul>
39
                {{ form }}
38
40
                <li>
39
                    <dl>
40
                        <dt>Access</dt>
41
                        <dd>
42
                            <select>
43
                                <option value="">{% trans 'Public or private' %}</option>
44
                                <option value="0">{% trans 'Public' %}</option>
45
                                <option value="0">{% trans 'Private' %}</option>
46
                            </select>
47
                        </dd>
48
                    </dl>
41
                    <input type="submit" value="OK" />
49
42
                </li>
50
43
            </ul>
44
            </form>
51
45
        </div>
46
        {% trans "You can search within repositories' names and summary, but not in their content nor their revisions" %}
52
47
53
48
        <div id="django_hg_main">
54
49
            <ul class="pager">
65
60
                        </span>
66
61
                    </h3>
67
62
                    <p class="description">{{ repo.summary }}</p>
63
                    <p class="float_left">
64
                      {% for member in repo.members %}
65
                          {% ifchanged member.permission %}
66
                              <em>{{ member.get_permission_display|capfirst }}</em><br />
67
                          {% endifchanged %}
68
                          - {{ member.user }}<br />
69
                      {% endfor %}
70
71
72
                    </p>
68
73
                    <ul class="changeset">
69
74
                    {% with repo.get_context as ctx %}
70
75
                      {% include 'django_hg/changeset_info.html' %}

Up to file-list templatetags/django_hg_tags.py:

1
1
# coding=utf-8
2
2
from datetime import datetime
3
import pytz
4
#from pytz import timezone
3
5
from math import ceil
4
6
5
7
from django.core.urlresolvers import reverse
8
from django.conf import settings as global_settings
6
9
from django.template import Library
7
10
11
8
12
register = Library()
9
13
10
14
@register.inclusion_tag('django_hg/path.html', takes_context=True)
@@ -60,6 +64,11 @@ def path(context):
60
64
61
65
@register.inclusion_tag('django_hg/filedisplay.html', takes_context=True)
62
66
def filedisplay(context):
67
    """
68
    Display a file at a given revision.
69
    According to the mimetype, the file may be displayed as source, colored
70
    thanks to Pygments, or, for pictures and PDF, directly in the browser
71
    """
63
72
    from pygments import highlight
64
73
    from pygments.formatters import HtmlFormatter
65
74
    from pygments.lexers import get_lexer_for_mimetype, guess_lexer_for_filename
@@ -107,9 +116,46 @@ def format_hg_date(value):
107
116
    As for now, the tz is not handled
108
117
109
118
    >>> format_hg_date([1234567890, -7200])
110
    '2009-02-14 00:31:30'
119
    '2009-02-14 02:31:30+01:00'
111
120
    """
112
    return '%(datetime)s' % { 'datetime': datetime.fromtimestamp(value[0]) }
121
    django_tz = pytz.timezone(global_settings.TIME_ZONE)
122
123
    return '%(datetime)s' % { 'datetime': django_tz.localize(datetime.fromtimestamp(value[0]-value[1])) }
124
125
@register.filter(name='format_hg_user')
126
def format_hg_user(value):
127
    """
128
    Return the user name from the Mercurial committer. Especially, try to hide
129
    the e-mail address if present
130
    >>> format_hg_user('Foo')
131
    'Foo'
132
133
    >>> format_hg_user('Foo bar')
134
    'Foo bar'
135
136
    >>> format_hg_user('Foo bar <foo@bar.com>')
137
    'Foo bar'
138
139
    >>> format_hg_user('Foo bar <foo@bar.com>, Baz <baz@bar.com>')
140
    'Foo bar, Baz'
141
142
    >>> format_hg_user('Foo, Bar, Baz')
143
    'Foo, Bar, Baz'
144
    """
145
    if value.find(',')> -1:
146
        elts = value.split(', ')
147
    else:
148
        elts = [value]
149
    names = []
150
    for elt in elts:
151
        parts = elt.split(' ')
152
        name = []
153
        for part in parts:
154
            if not part.startswith('<'):
155
                name.append(part)
156
        names.append(' '.join(name))
157
158
    return ', '.join(names)
113
159
114
160
@register.inclusion_tag('django_hg/pagination.html', takes_context=True)
115
161
def paginate(context):
@@ -210,7 +256,20 @@ def paginate(context):
210
256
211
257
@register.filter
212
258
def strip_path(value):
213
    """ A pythonic version of PHP basename"""
259
    """
260
    A pythonic version of PHP basename
261
    >>> strip_path('foo')
262
    'foo'
263
264
    >>> strip_path('foo/bar')
265
    'bar'
266
267
    >>> strip_path('foo/bar/')
268
    'bar'
269
    """
270
    if value.rfind('/') == len(value)-1:
271
        value = value[:value.rfind('/')]
272
214
273
    return value[value.rfind('/')+1:]
215
274
216
275
if __name__ == "__main__":

Up to file-list views.py:

@@ -12,6 +12,7 @@ from django.conf import settings as glob
12
12
from django.template import RequestContext
13
13
from django_hg.models import HgContext, HgRepository
14
14
from django_hg.decorators import logged_in_or_basicauth
15
from django_hg.forms import HgRepositoryForm
15
16
16
17
from mercurial.error import RepoError
17
18
@@ -282,10 +283,22 @@ def list(request):
282
283
    only public repositories
283
284
284
285
    """
286
    #if request.GET.get('search'):
287
    form = HgRepositoryForm({'search': request.GET.get('search'),
288
                             'display': request.GET.get('display')})
289
    if form.is_valid():
290
        search = form.data['search']
291
        display = form.data['display']
292
                #'?search='+form.cleaned_data['name'] +
293
                #'&anonymous_access=' + str(form.cleaned_data['anonymous_access']))
294
    else:
295
        search = None
296
        display= 'all'
297
285
298
    page = int(request.GET.get('page', 1))
286
299
    if page <= 0 :
287
300
        page = 1
288
    max = HgRepository.objects.count_for_user(request.user, 1)
301
    max = HgRepository.objects.count_for_user(request.user, 1, search=search, display=display)
289
302
    if ceil(float(max)/float(global_settings.DJANGO_HG_PAGER_ITEMS))<page:
290
303
        page = int(ceil(float(max)/float(global_settings.DJANGO_HG_PAGER_ITEMS)))
291
304
    start = (page-1)*global_settings.DJANGO_HG_PAGER_ITEMS
@@ -295,7 +308,7 @@ def list(request):
295
308
296
309
    repositories = []
297
310
    if (max > 0):
298
        q = HgRepository.objects.get_for_user(request.user, 1)[start:end]
311
        q = HgRepository.objects.get_for_user(request.user, 1, search=search, display=display)[start:end]
299
312
        for repo in q:
300
313
            try:
301
314
                repo.set_context(HgContext(repo, 'tip'))
@@ -307,12 +320,14 @@ def list(request):
307
320
            except:
308
321
                pass
309
322
323
310
324
    return render_to_response('django_hg/list.html', {
311
325
        'end': end,
312
326
        'host': request.META['HTTP_HOST'],
313
327
        'items_per_page': global_settings.DJANGO_HG_PAGER_ITEMS,
314
328
        'repositories': repositories,
315
329
        'page': page,
330
        'form': form.as_ul(),
316
331
        'start': start+1,
317
332
        'max': int(max),
318
333
    }, context_instance=RequestContext(request))