models.py 13.1 KB
Newer Older
Jeremy Kerr's avatar
Jeremy Kerr committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Patchwork - automated patch tracking system
# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
#
# This file is part of the Patchwork package.
#
# Patchwork is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Patchwork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from django.db import models
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.contrib.sites.models import Site
Jeremy Kerr's avatar
Jeremy Kerr committed
24
from patchwork.parser import hash_patch
Jeremy Kerr's avatar
Jeremy Kerr committed
25
26
27
28

import re
import datetime, time
import random
29
30

try:
31
32
    from email.mime.nonmultipart import MIMENonMultipart
    from email.encoders import encode_7or8bit
33
    from email.parser import HeaderParser
34
35
36
    import email.utils
except ImportError:
    # Python 2.4 compatibility
37
38
    from email.MIMENonMultipart import MIMENonMultipart
    from email.Encoders import encode_7or8bit
39
    from email.Parser import HeaderParser
40
41
    import email.Utils
    email.utils = email.Utils
Jeremy Kerr's avatar
Jeremy Kerr committed
42
43
44

class Person(models.Model):
    email = models.CharField(max_length=255, unique = True)
45
46
    name = models.CharField(max_length=255, null = True, blank = True)
    user = models.ForeignKey(User, null = True, blank = True)
Jeremy Kerr's avatar
Jeremy Kerr committed
47

48
    def __unicode__(self):
Jeremy Kerr's avatar
Jeremy Kerr committed
49
        if self.name:
50
            return u'%s <%s>' % (self.name, self.email)
Jeremy Kerr's avatar
Jeremy Kerr committed
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
        else:
            return self.email

    def link_to_user(self, user):
        self.name = user.get_profile().name()
        self.user = user

    class Meta:
        verbose_name_plural = 'People'

class Project(models.Model):
    linkname = models.CharField(max_length=255, unique=True)
    name = models.CharField(max_length=255, unique=True)
    listid = models.CharField(max_length=255, unique=True)
    listemail = models.CharField(max_length=200)

67
    def __unicode__(self):
Jeremy Kerr's avatar
Jeremy Kerr committed
68
69
        return self.name

70
71
72
73
74
    def is_editable(self, user):
        if not user.is_authenticated():
            return False
        return self in user.get_profile().maintainer_projects.all()

Jeremy Kerr's avatar
Jeremy Kerr committed
75
76
class UserProfile(models.Model):
    user = models.ForeignKey(User, unique = True)
77
    primary_project = models.ForeignKey(Project, null = True, blank = True)
Jeremy Kerr's avatar
Jeremy Kerr committed
78
79
80
81
82
83
84
85
86
87
88
    maintainer_projects = models.ManyToManyField(Project,
            related_name = 'maintainer_project')
    send_email = models.BooleanField(default = False,
            help_text = 'Selecting this option allows patchwork to send ' +
                'email on your behalf')
    patches_per_page = models.PositiveIntegerField(default = 100,
            null = False, blank = False,
            help_text = 'Number of patches to display per page')

    def name(self):
        if self.user.first_name or self.user.last_name:
89
            names = filter(bool, [self.user.first_name, self.user.last_name])
90
            return u' '.join(names)
Jeremy Kerr's avatar
Jeremy Kerr committed
91
92
93
94
        return self.user.username

    def contributor_projects(self):
        submitters = Person.objects.filter(user = self.user)
95
96
97
98
        return Project.objects.filter(id__in =
                                        Patch.objects.filter(
                                            submitter__in = submitters)
                                        .values('project_id').query)
Jeremy Kerr's avatar
Jeremy Kerr committed
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116


    def sync_person(self):
        pass

    def n_todo_patches(self):
        return self.todo_patches().count()

    def todo_patches(self, project = None):

        # filter on project, if necessary
        if project:
            qs = Patch.objects.filter(project = project)
        else:
            qs = Patch.objects

        qs = qs.filter(archived = False) \
             .filter(delegate = self.user) \
117
118
             .filter(state__in =
                     State.objects.filter(action_required = True)
Jeremy Kerr's avatar
Jeremy Kerr committed
119
120
121
122
                         .values('pk').query)
        return qs

    def save(self):
123
124
125
126
127
        super(UserProfile, self).save()
        people = Person.objects.filter(email = self.user.email)
        if not people:
            person = Person(email = self.user.email,
                    name = self.name(), user = self.user)
Jeremy Kerr's avatar
Jeremy Kerr committed
128
            person.save()
129
130
        else:
            for person in people:
131
                 person.link_to_user(self.user)
132
                 person.save()
Jeremy Kerr's avatar
Jeremy Kerr committed
133

134
    def __unicode__(self):
Jeremy Kerr's avatar
Jeremy Kerr committed
135
136
        return self.name()

137
138
139
140
141
142
143
144
def _user_created_callback(sender, created, instance, **kwargs):
    if not created:
        return
    profile = UserProfile(user = instance)
    profile.save()

models.signals.post_save.connect(_user_created_callback, sender = User)

Jeremy Kerr's avatar
Jeremy Kerr committed
145
146
147
148
149
class State(models.Model):
    name = models.CharField(max_length = 100)
    ordering = models.IntegerField(unique = True)
    action_required = models.BooleanField(default = True)

150
    def __unicode__(self):
Jeremy Kerr's avatar
Jeremy Kerr committed
151
152
153
154
155
        return self.name

    class Meta:
        ordering = ['ordering']

Jeremy Kerr's avatar
Jeremy Kerr committed
156
class HashField(models.CharField):
Jeremy Kerr's avatar
Jeremy Kerr committed
157
158
159
160
    __metaclass__ = models.SubfieldBase

    def __init__(self, algorithm = 'sha1', *args, **kwargs):
        self.algorithm = algorithm
Nate Case's avatar
Nate Case committed
161
162
        try:
            import hashlib
Jeremy Kerr's avatar
Jeremy Kerr committed
163
164
165
            def _construct(string = ''):
                return hashlib.new(self.algorithm, string)
            self.construct = _construct
166
            self.n_bytes = len(hashlib.new(self.algorithm).hexdigest())
Nate Case's avatar
Nate Case committed
167
        except ImportError:
Jeremy Kerr's avatar
Jeremy Kerr committed
168
169
170
            modules = { 'sha1': 'sha', 'md5': 'md5'}

            if algorithm not in modules.keys():
Nate Case's avatar
Nate Case committed
171
                raise NameError("Unknown algorithm '%s'" % algorithm)
Jeremy Kerr's avatar
Jeremy Kerr committed
172
173
174
175

            self.construct = __import__(modules[algorithm]).new

        self.n_bytes = len(self.construct().hexdigest())
Jeremy Kerr's avatar
Jeremy Kerr committed
176

177
        kwargs['max_length'] = self.n_bytes
Jeremy Kerr's avatar
Jeremy Kerr committed
178
179
180
        super(HashField, self).__init__(*args, **kwargs)

    def db_type(self):
181
        return 'char(%d)' % self.n_bytes
Jeremy Kerr's avatar
Jeremy Kerr committed
182

183
184
185
186
187
188
189
190
class PatchMbox(MIMENonMultipart):
    patch_charset = 'utf-8'
    def __init__(self, _text):
        MIMENonMultipart.__init__(self, 'text', 'plain',
                        **{'charset': self.patch_charset})
        self.set_payload(_text.encode(self.patch_charset))
        encode_7or8bit(self)

Jeremy Kerr's avatar
Jeremy Kerr committed
191
192
class Patch(models.Model):
    project = models.ForeignKey(Project)
193
    msgid = models.CharField(max_length=255)
Jeremy Kerr's avatar
Jeremy Kerr committed
194
195
196
197
198
199
200
    name = models.CharField(max_length=255)
    date = models.DateTimeField(default=datetime.datetime.now)
    submitter = models.ForeignKey(Person)
    delegate = models.ForeignKey(User, blank = True, null = True)
    state = models.ForeignKey(State)
    archived = models.BooleanField(default = False)
    headers = models.TextField(blank = True)
201
202
    content = models.TextField(null = True, blank = True)
    pull_url = models.CharField(max_length=255, null = True, blank = True)
Jeremy Kerr's avatar
Jeremy Kerr committed
203
    commit_ref = models.CharField(max_length=255, null = True, blank = True)
204
    hash = HashField(null = True, blank = True)
Jeremy Kerr's avatar
Jeremy Kerr committed
205

206
    def __unicode__(self):
Jeremy Kerr's avatar
Jeremy Kerr committed
207
208
209
        return self.name

    def comments(self):
210
        return Comment.objects.filter(patch = self)
Jeremy Kerr's avatar
Jeremy Kerr committed
211
212

    def save(self):
213
        try:
Jeremy Kerr's avatar
Jeremy Kerr committed
214
215
216
            s = self.state
        except:
            self.state = State.objects.get(ordering =  0)
Jeremy Kerr's avatar
Jeremy Kerr committed
217

218
        if self.hash is None and self.content is not None:
Jeremy Kerr's avatar
Jeremy Kerr committed
219
            self.hash = hash_patch(self.content).hexdigest()
Jeremy Kerr's avatar
Jeremy Kerr committed
220

Jeremy Kerr's avatar
Jeremy Kerr committed
221
222
223
224
225
226
227
228
229
        super(Patch, self).save()

    def is_editable(self, user):
        if not user.is_authenticated():
            return False

        if self.submitter.user == user or self.delegate == user:
            return True

230
        return self.project.is_editable(user)
Jeremy Kerr's avatar
Jeremy Kerr committed
231
232
233
234
235
236
237

    def filename(self):
        fname_re = re.compile('[^-_A-Za-z0-9\.]+')
        str = fname_re.sub('-', self.name)
        return str.strip('-') + '.patch'

    def mbox(self):
238
239
        postscript_re = re.compile('\n-{2,3} ?\n')

Jeremy Kerr's avatar
Jeremy Kerr committed
240
241
        comment = None
        try:
Jeremy Kerr's avatar
Jeremy Kerr committed
242
            comment = Comment.objects.get(patch = self, msgid = self.msgid)
Jeremy Kerr's avatar
Jeremy Kerr committed
243
244
245
246
247
        except Exception:
            pass

        body = ''
        if comment:
248
249
            body = comment.content.strip() + "\n"

250
251
252
253
254
255
256
257
        parts = postscript_re.split(body, 1)
        if len(parts) == 2:
            (body, postscript) = parts
            body = body.strip() + "\n"
            postscript = postscript.strip() + "\n"
        else:
            postscript = ''

258
259
260
261
262
263
264
        for comment in Comment.objects.filter(patch = self) \
                .exclude(msgid = self.msgid):
            body += comment.patch_responses()

        if body:
            body += '\n'

265
266
267
        if postscript:
            body += '---\n' + postscript.strip() + '\n'

268
269
        if self.content:
            body += '\n' + self.content
Jeremy Kerr's avatar
Jeremy Kerr committed
270

271
        mail = PatchMbox(body)
Jeremy Kerr's avatar
Jeremy Kerr committed
272
273
274
        mail['Subject'] = self.name
        mail['Date'] = email.utils.formatdate(
                        time.mktime(self.date.utctimetuple()))
275
        mail['From'] = unicode(self.submitter)
Jeremy Kerr's avatar
Jeremy Kerr committed
276
        mail['X-Patchwork-Id'] = str(self.id)
277
        mail['Message-Id'] = self.msgid
Jeremy Kerr's avatar
Jeremy Kerr committed
278
279
280
        mail.set_unixfrom('From patchwork ' + self.date.ctime())


281
282
283
284
285
286
287
        copied_headers = ['To', 'Cc']
        orig_headers = HeaderParser().parsestr(str(self.headers))
        for header in copied_headers:
            if header in orig_headers:
                mail[header] = orig_headers[header]

        return mail
Jeremy Kerr's avatar
Jeremy Kerr committed
288
289
290
291
292
293
294
295

    @models.permalink
    def get_absolute_url(self):
        return ('patchwork.views.patch.patch', (), {'patch_id': self.id})

    class Meta:
        verbose_name_plural = 'Patches'
        ordering = ['date']
296
        unique_together = [('msgid', 'project')]
Jeremy Kerr's avatar
Jeremy Kerr committed
297
298
299

class Comment(models.Model):
    patch = models.ForeignKey(Patch)
300
    msgid = models.CharField(max_length=255)
Jeremy Kerr's avatar
Jeremy Kerr committed
301
302
303
304
305
    submitter = models.ForeignKey(Person)
    date = models.DateTimeField(default = datetime.datetime.now)
    headers = models.TextField(blank = True)
    content = models.TextField()

306
307
308
    response_re = re.compile( \
            '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$',
            re.M | re.I)
309
310

    def patch_responses(self):
311
        return ''.join([ match.group(0) + '\n' for match in
312
313
                                self.response_re.finditer(self.content)])

Jeremy Kerr's avatar
Jeremy Kerr committed
314
315
    class Meta:
        ordering = ['date']
316
        unique_together = [('msgid', 'patch')]
Jeremy Kerr's avatar
Jeremy Kerr committed
317
318
319
320
321

class Bundle(models.Model):
    owner = models.ForeignKey(User)
    project = models.ForeignKey(Project)
    name = models.CharField(max_length = 50, null = False, blank = False)
322
    patches = models.ManyToManyField(Patch, through = 'BundlePatch')
Jeremy Kerr's avatar
Jeremy Kerr committed
323
324
325
326
327
    public = models.BooleanField(default = False)

    def n_patches(self):
        return self.patches.all().count()

Jeremy Kerr's avatar
Jeremy Kerr committed
328
    def ordered_patches(self):
Guilherme Salgado's avatar
Guilherme Salgado committed
329
        return self.patches.order_by('bundlepatch__order')
Jeremy Kerr's avatar
Jeremy Kerr committed
330

331
332
    def append_patch(self, patch):
        # todo: use the aggregate queries in django 1.1
Jeremy Kerr's avatar
Jeremy Kerr committed
333
334
335
336
337
338
339
340
341
342
343
        orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \
                 .values('order')

        if len(orders) > 0:
            max_order = orders[0]['order']
        else:
            max_order = 0

        # see if the patch is already in this bundle
        if BundlePatch.objects.filter(bundle = self, patch = patch).count():
            raise Exception("patch is already in bundle")
344

Jeremy Kerr's avatar
Jeremy Kerr committed
345
346
        bp = BundlePatch.objects.create(bundle = self, patch = patch,
                order = max_order + 1)
347
348
        bp.save()

Jeremy Kerr's avatar
Jeremy Kerr committed
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
    class Meta:
        unique_together = [('owner', 'name')]

    def public_url(self):
        if not self.public:
            return None
        site = Site.objects.get_current()
        return 'http://%s%s' % (site.domain,
                reverse('patchwork.views.bundle.public',
                        kwargs = {
                                'username': self.owner.username,
                                'bundlename': self.name
                        }))

    def mbox(self):
364
        return '\n'.join([p.mbox().as_string(True)
Jeremy Kerr's avatar
Jeremy Kerr committed
365
                        for p in self.ordered_patches()])
Jeremy Kerr's avatar
Jeremy Kerr committed
366

367
368
369
370
371
372
class BundlePatch(models.Model):
    patch = models.ForeignKey(Patch)
    bundle = models.ForeignKey(Bundle)
    order = models.IntegerField()

    class Meta:
Jeremy Kerr's avatar
Jeremy Kerr committed
373
374
        unique_together = [('bundle', 'patch')]
        ordering = ['order']
375

376
377
378
379
380
381
382
383
384
385
386
387
class UserPersonConfirmation(models.Model):
    user = models.ForeignKey(User)
    email = models.CharField(max_length = 200)
    key = HashField()
    date = models.DateTimeField(default=datetime.datetime.now)
    active = models.BooleanField(default = True)

    def confirm(self):
        if not self.active:
            return
        person = None
        try:
388
            person = Person.objects.get(email__iexact = self.email)
389
390
391
392
393
394
395
396
        except Exception:
            pass
        if not person:
            person = Person(email = self.email)

        person.link_to_user(self.user)
        person.save()
        self.active = False
397
        self.save()
398
399
400
401
402
403
404
405
406

    def save(self):
        max = 1 << 32
        if self.key == '':
            str = '%s%s%d' % (self.user, self.email, random.randint(0, max))
            self.key = self._meta.get_field('key').construct(str).hexdigest()
        super(UserPersonConfirmation, self).save()