Switch to unified view

a/Allura/allura/model/stats.py b/Allura/allura/model/stats.py
1
import logging
2
3
import ming
1
import pymongo
2
from pylons import tmpl_context as c, app_globals as g
3
from pylons import request
4
4
5
import bson
6
from ming import schema as S
7
from ming import Field, Index, collection
8
from ming.orm import session, state, Mapper
9
from ming.orm import FieldProperty
10
from ming.orm.declarative import MappedClass
11
from datetime import datetime, timedelta
12
import difflib
13
5
from .session import main_doc_session
14
from allura.model.session import main_orm_session
15
from allura.lib import helpers as h
6
16
7
log = logging.getLogger(__name__)
17
class Stats(MappedClass):
8
9
class Stats(ming.Document):
10
    class __mongometa__:
18
    class __mongometa__:
19
        name='basestats'
11
        session = main_doc_session
20
        session = main_orm_session
12
        name='stats'
21
        unique_indexes = [ '_id']
13
22
23
    _id=FieldProperty(S.ObjectId)
24
25
    visible = FieldProperty(bool, if_missing = True)
26
    registration_date = FieldProperty(datetime)
27
    general = FieldProperty([dict(
28
        category = S.ObjectId,
29
        messages = [dict(
30
            messagetype = str,
31
            created = int,
32
            modified = int)],
33
        tickets = dict(
34
            solved = int,
35
            assigned = int,
36
            revoked = int,
37
            totsolvingtime = int),
38
        commits = [dict(
39
            lines = int,
40
            number = int,
41
            language = S.ObjectId)])])
42
43
    lastmonth=FieldProperty(dict(
44
        messages=[dict(
45
            datetime=datetime,
46
            created=bool,
47
            categories=[S.ObjectId],
48
            messagetype=str)],
49
        assignedtickets=[dict(
50
            datetime=datetime,
51
            categories=[S.ObjectId])],
52
        revokedtickets=[dict(
53
            datetime=datetime,
54
            categories=[S.ObjectId])],
55
        solvedtickets=[dict(
56
            datetime=datetime,
57
            categories=[S.ObjectId],
58
            solvingtime=int)],
59
        commits=[dict(
60
            datetime=datetime,
61
            categories=[S.ObjectId],
62
            programming_languages=[S.ObjectId],
63
            lines=int)]))
64
65
    def getCodeContribution(self):
66
        days=(datetime.today() - self.registration_date).days
67
        if not days:
68
            days=1
69
        for val in self['general']:
70
            if val['category'] is None:
71
                for commits in val['commits']:
72
                    if commits['language'] is None: 
73
                        if days > 30:
74
                            return round(float(commits.lines)/days*30, 2)
75
                        else:
76
                            return float(commits.lines)
77
        return 0
78
79
    def getDiscussionContribution(self):
80
        days=(datetime.today() - self.registration_date).days
81
        if not days:
82
            days=1
83
        for val in self['general']:
84
            if val['category'] is None:
85
                for artifact in val['messages']:
86
                    if artifact['messagetype'] is None: 
87
                        tot = artifact.created+artifact.modified
88
                        if days > 30:
89
                            return round(float(tot)/days*30,2)
90
                        else:
91
                            return float(tot)
92
        return 0
93
94
    def getTicketsContribution(self):
95
        for val in self['general']:
96
            if val['category'] is None:
97
                tickets = val['tickets']
98
                if tickets.assigned == 0:
99
                    return 0
100
                return float(tickets.solved) / tickets.assigned
101
        return 0
102
103
    @classmethod
104
    def getMaxAndAverageCodeContribution(self):
105
        res = self.query.find()
106
        n = res.count()
107
        if n == 0:
108
            return 0, 0
109
        maxcontribution=max([x.getCodeContribution() for x in res])
110
        averagecontribution=sum([x.getCodeContribution() for x in res]) / n
111
        return maxcontribution, round(averagecontribution, 2)
112
113
    @classmethod
114
    def getMaxAndAverageDiscussionContribution(self):
115
        res = self.query.find()
116
        n = res.count()
117
        if n == 0:
118
            return 0, 0
119
        maxcontribution=max([x.getDiscussionContribution() for x in res])
120
        averagecontribution=sum([x.getDiscussionContribution() for x in res])/n
121
        return maxcontribution, round(averagecontribution, 2)
122
123
    @classmethod
124
    def getMaxAndAverageTicketsSolvingPercentage(self):
125
        res = self.query.find()
126
        n = res.count()
127
        if n == 0:
128
            return 0, 0
129
        maxcontribution=max([x.getTicketsContribution() for x in res])
130
        averagecontribution=sum([x.getTicketsContribution() for x in res])/n
131
        return maxcontribution, round(averagecontribution, 2)
132
133
    def codeRanking(self):
134
        res = self.query.find()
135
        totn = res.count()
136
        codcontr = self.getCodeContribution()
137
        upper = len([x for x in res if x.getCodeContribution() > codcontr])
138
        return round((totn - upper) * 100.0 / totn, 2)
139
140
    def discussionRanking(self):
141
        res = self.query.find()
142
        totn = res.count()
143
        disccontr = self.getDiscussionContribution()
144
        upper=len([x for x in res if x.getDiscussionContribution()>disccontr])
145
        return round((totn - upper) * 100.0 / totn, 2)
146
147
    def ticketsRanking(self):
148
        res = self.query.find()
149
        totn = res.count()
150
        ticketscontr = self.getTicketsContribution()
151
        upper=len([x for x in res if x.getTicketsContribution()>ticketscontr])
152
        return round((totn - upper) * 100.0 / totn, 2)
153
154
    def getCommits(self, category = None):
155
        i = getElementIndex(self.general, category = category)
156
        if i is None: 
157
            return dict(number=0, lines=0)
158
        cat = self.general[i]
159
        j = getElementIndex(cat.commits, language = None)
160
        if j is None:
161
            return dict(number=0, lines=0)
162
        return dict(
163
            number=cat.commits[j]['number'], 
164
            lines=cat.commits[j]['lines'])
165
166
    def getArtifacts(self, category = None, art_type = None):
167
        i = getElementIndex(self.general, category = category)
168
        if i is None:
169
            return dict(created=0, modified=0)
170
        cat = self.general[i]
171
        j = getElementIndex(cat.messages, messagetype = art_type)
172
        if j is None:
173
            return dict(created=0, modified=0)
174
        return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)
175
176
    def getTickets(self, category = None):
177
        i = getElementIndex(self.general, category = category)
178
        if i is None:
179
            return dict(
180
                assigned=0,
181
                solved=0,
182
                revoked=0,
183
                averagesolvingtime=None)
184
        if self.general[i].tickets.solved > 0:
185
            tot = self.general[i].tickets.totsolvingtime 
186
            number = self.general[i].tickets.solved
187
            average = tot / number
188
        else: 
189
            average = None
190
        return dict(
191
            assigned=self.general[i].tickets.assigned,
192
            solved=self.general[i].tickets.solved,
193
            revoked=self.general[i].tickets.revoked,
194
            averagesolvingtime=_convertTimeDiff(average))
195
196
    def getCommitsByCategory(self):
197
        from allura.model.project import TroveCategory
198
199
        by_cat = {}
200
        for entry in self.general:
201
            cat = entry.category
202
            i = getElementIndex(entry.commits, language = None)
203
            if i is None: 
204
                n, lines = 0, 0
205
            else: 
206
                n, lines = entry.commits[i].number, entry.commits[i].lines
207
            if cat != None:
208
                cat = TroveCategory.query.get(_id = cat)
209
            by_cat[cat] = dict(number=n, lines=lines)
210
        return by_cat
211
212
    #For the moment, commit stats by language are not used, since each project
213
    #can be linked to more than one programming language and we don't know how
214
    #to which programming language should be credited a line of code modified
215
    #within a project including two or more languages.
216
    def getCommitsByLanguage(self):
217
        langlist = []
218
        by_lang = {}
219
        i = getElementIndex(self.general, category=None)
220
        if i is None: 
221
            return dict(number=0, lines=0)
222
        return dict([(el.language, dict(lines=el.lines, number=el.number))
223
                     for el in self.general[i].commits])
224
225
    def getArtifactsByCategory(self, detailed=False):
226
        from allura.model.project import TroveCategory
227
228
        by_cat = {}
229
        for entry in self.general:
230
            cat = entry.category
231
            if cat != None: 
232
                cat = TroveCategory.query.get(_id = cat)
233
            if detailed: 
234
                by_cat[cat] = entry.messages
235
            else:
236
                i = getElementIndex(entry.messages, messagetype=None)
237
                if i is not None:
238
                    by_cat[cat] = entry.messages[i]
239
                else: 
240
                    by_cat[cat] = dict(created=0, modified=0)
241
        return by_cat
242
243
    def getArtifactsByType(self, category=None):
244
        i = getElementIndex(self.general, category = category)
245
        if i is None: 
246
            return {}
247
        entry = self.general[i].messages
248
        by_type = dict([(el.messagetype, dict(created=el.created,
249
                                              modified=el.modified))
250
                         for el in entry])
251
        return by_type
252
253
    def getTicketsByCategory(self):
254
        from allura.model.project import TroveCategory
255
256
        by_cat = {}
257
        for entry in self.general:
258
            cat = entry.category
259
            if cat != None:
260
                cat = TroveCategory.query.get(_id = cat)
261
            a, s = entry.tickets.assigned, entry.tickets.solved
262
            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
263
            if s:
264
                average = time / s
265
            else:
266
                average = None
267
            by_cat[cat] = dict(
268
                assigned=a,
269
                solved=s,
270
                revoked=r, 
271
                averagesolvingtime=_convertTimeDiff(average))
272
        return by_cat
273
274
    def getLastMonthCommits(self, category = None):
275
        self.checkOldArtifacts() 
276
        lineslist = [el.lines for el in self.lastmonth.commits
277
                     if category in el.categories + [None]]
278
        return dict(number=len(lineslist), lines=sum(lineslist))
279
280
    def getLastMonthCommitsByCategory(self):
281
        from allura.model.project import TroveCategory
282
283
        self.checkOldArtifacts() 
284
        seen = set()
285
        catlist=[el.category for el in self.general
286
                 if el.category not in seen and not seen.add(el.category)]
287
288
        by_cat = {}
289
        for cat in catlist:
290
            lineslist = [el.lines for el in self.lastmonth.commits
291
                         if cat in el.categories + [None]]
292
            n = len(lineslist)
293
            lines = sum(lineslist)
294
            if cat != None:
295
                cat = TroveCategory.query.get(_id = cat)
296
            by_cat[cat] = dict(number=n, lines=lines)
297
        return by_cat
298
299
    def getLastMonthCommitsByLanguage(self):
300
        from allura.model.project import TroveCategory
301
302
        self.checkOldArtifacts() 
303
        seen = set()
304
        langlist=[el.language for el in self.general
305
                  if el.language not in seen and not seen.add(el.language)]
306
307
        by_lang = {}
308
        for lang in langlist:
309
            lineslist = [el.lines for el in self.lastmonth.commits
310
                         if lang in el.programming_languages + [None]]
311
            n = len(lineslist)
312
            lines = sum(lineslist)
313
            if lang != None:
314
                lang = TroveCategory.query.get(_id = lang)
315
            by_lang[lang] = dict(number=n, lines=lines)
316
        return by_lang
317
318
    def getLastMonthArtifacts(self, category = None, art_type = None):
319
        self.checkOldArtifacts() 
320
        cre, mod = reduce(
321
            addtuple, 
322
            [(int(el.created),1-int(el.created))
323
                for el in self.lastmonth.messages
324
                if (category is None or category in el.categories) and 
325
                (el.messagetype == art_type or art_type is None)], 
326
            (0,0))
327
        return dict(created=cre, modified=mod)
328
329
    def getLastMonthArtifactsByType(self, category = None):
330
        self.checkOldArtifacts()
331
        seen = set()
332
        types=[el.messagetype for el in self.lastmonth.messages
333
               if el.messagetype not in seen and not seen.add(el.messagetype)]
334
335
        by_type = {}
336
        for t in types:
337
            cre, mod = reduce(
338
                addtuple, 
339
                [(int(el.created),1-int(el.created))
340
                 for el in self.lastmonth.messages
341
                 if el.messagetype == t and
342
                 category in [None]+el.categories],
343
                (0,0))
344
            by_type[t] = dict(created=cre, modified=mod)
345
        return by_type
346
347
    def getLastMonthArtifactsByCategory(self):
348
        from allura.model.project import TroveCategory
349
350
        self.checkOldArtifacts() 
351
        seen = set()
352
        catlist=[el.category for el in self.general
353
                 if el.category not in seen and not seen.add(el.category)]
354
355
        by_cat = {}
356
        for cat in catlist:
357
            cre, mod = reduce(
358
                addtuple, 
359
                [(int(el.created),1-int(el.created))
360
                 for el in self.lastmonth.messages 
361
                 if cat in el.categories + [None]], (0,0))
362
            if cat != None:
363
                cat = TroveCategory.query.get(_id = cat)
364
            by_cat[cat] = dict(created=cre, modified=mod)
365
        return by_cat
366
367
    def getLastMonthTickets(self, category = None):
368
        from allura.model.project import TroveCategory
369
370
        self.checkOldArtifacts()
371
        a = len([el for el in self.lastmonth.assignedtickets
372
                 if category in el.categories + [None]])
373
        r = len([el for el in self.lastmonth.revokedtickets
374
                 if category in el.categories + [None]])
375
        s, time = reduce(
376
            addtuple, 
377
            [(1, el.solvingtime)
378
             for el in self.lastmonth.solvedtickets
379
             if category in el.categories + [None]],
380
            (0,0))
381
        if category!=None:
382
            category = TroveCategory.query.get(_id=category)
383
        if s > 0:
384
            time = time / s
385
        else:
386
            time = None
387
        return dict(
388
            assigned=a,
389
            revoked=r,
390
            solved=s, 
391
            averagesolvingtime=_convertTimeDiff(time))
392
        
393
    def getLastMonthTicketsByCategory(self):
394
        from allura.model.project import TroveCategory
395
396
        self.checkOldArtifacts()
397
        seen = set()
398
        catlist=[el.category for el in self.general
399
                 if el.category not in seen and not seen.add(el.category)]
400
        by_cat = {}
401
        for cat in catlist:
402
            a = len([el for el in self.lastmonth.assignedtickets
403
                     if cat in el.categories + [None]])
404
            r = len([el for el in self.lastmonth.revokedtickets
405
                     if cat in el.categories + [None]])
406
            s, time = reduce(addtuple, [(1, el.solvingtime)
407
                                        for el in self.lastmonth.solvedtickets
408
                                        if cat in el.categories+[None]],(0,0))
409
            if cat != None:
410
                cat = TroveCategory.query.get(_id = cat)
411
            if s > 0: 
412
                time = time / s
413
            else:
414
                time = None
415
            by_cat[cat] = dict(
416
                assigned=a,
417
                revoked=r,
418
                solved=s, 
419
                averagesolvingtime=_convertTimeDiff(time))
420
        return by_cat
421
        
422
    def checkOldArtifacts(self):
423
        now = datetime.utcnow()
424
        for m in self.lastmonth.messages:
425
            if now - m.datetime > timedelta(30):
426
                self.lastmonth.messages.remove(m)
427
        for t in self.lastmonth.assignedtickets:
428
            if now - t.datetime > timedelta(30):
429
                self.lastmonth.assignedtickets.remove(t)
430
        for t in self.lastmonth.revokedtickets:
431
            if now - t.datetime > timedelta(30):
432
                self.lastmonth.revokedtickets.remove(t)
433
        for t in self.lastmonth.solvedtickets:
434
            if now - t.datetime > timedelta(30):
435
                self.lastmonth.solvedtickets.remove(t)
436
        for c in self.lastmonth.commits:
437
            if now - c.datetime > timedelta(30):
438
                self.lastmonth.commits.remove(c)
439
440
    def addNewArtifact(self, art_type, art_datetime, project):
441
        self._updateArtifactsStats(art_type, art_datetime, project, "created")
442
443
    def addModifiedArtifact(self, art_type, art_datetime, project):
444
        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
445
446
    def addAssignedTicket(self, ticket_datetime, project):
447
        topics = [t for t in project.trove_topic if t]
448
        self._updateTicketsStats(topics, 'assigned')
449
        self.lastmonth.assignedtickets.append(
450
            dict(datetime=ticket_datetime, categories=topics))
451
452
    def addRevokedTicket(self, ticket_datetime, project):
453
        topics = [t for t in project.trove_topic if t]
454
        self._updateTicketsStats(topics, 'revoked')
455
        self.lastmonth.revokedtickets.append(
456
            dict(datetime=ticket_datetime, categories=topics))
457
        self.checkOldArtifacts()
458
459
    def addClosedTicket(self, open_datetime, close_datetime, project):
460
        topics = [t for t in project.trove_topic if t]
461
        s_time=int((close_datetime-open_datetime).total_seconds())
462
        self._updateTicketsStats(topics, 'solved', s_time = s_time)
463
        self.lastmonth.solvedtickets.append(dict(
464
            datetime=close_datetime,
465
            categories=topics,
466
            solvingtime=s_time))
467
        self.checkOldArtifacts()
468
469
    def addCommit(self, newcommit, commit_datetime, project):
470
        def _computeLines(newblob, oldblob = None):
471
            if oldblob:
472
                listold = list(oldblob)
473
            else:
474
                listold = []
475
            if newblob:
476
                listnew = list(newblob)
477
            else:
478
                listnew = []
479
480
            if oldblob is None:
481
                lines = len(listnew)
482
            elif newblob and newblob.has_html_view:
483
                diff = difflib.unified_diff(
484
                    listold, listnew,
485
                    ('old' + oldblob.path()).encode('utf-8'),
486
                    ('new' + newblob.path()).encode('utf-8'))
487
                lines = len([l for l in diff if len(l) > 0 and l[0] == '+'])-1
488
            else:
489
                lines = 0
490
            return lines
491
492
        def _addCommitData(stats, topics, languages, lines):          
493
            lt = topics + [None]
494
            ll = languages + [None]
495
            for t in lt:
496
                i = getElementIndex(stats.general, category=t) 
497
                if i is None:
498
                    newstats = dict(
499
                        category=t,
500
                        commits=[],
501
                        messages=[],
502
                        tickets=dict(
503
                            assigned=0,
504
                            solved=0,
505
                            revoked=0,
506
                            totsolvingtime=0))
507
                    stats.general.append(newstats)
508
                    i = getElementIndex(stats.general, category=t)
509
                for lang in ll:
510
                    j = getElementIndex(
511
                        stats.general[i]['commits'], language=lang)
512
                    if j is None:
513
                        stats.general[i]['commits'].append(dict(
514
                            language=lang, lines=lines, number=1))
515
                    else:
516
                        stats.general[i]['commits'][j].lines += lines
517
                        stats.general[i]['commits'][j].number += 1
518
519
        topics = [t for t in project.trove_topic if t]
520
        languages = [l for l in project.trove_language if l]
521
522
        d = newcommit.diffs
523
        if len(newcommit.parent_ids) > 0:
524
            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
525
526
        totlines = 0
527
        for changed in d.changed:
528
            newblob = newcommit.tree.get_blob_by_path(changed)
529
            oldblob = oldcommit.tree.get_blob_by_path(changed)
530
            totlines+=_computeLines(newblob, oldblob)
531
532
        for copied in d.copied:
533
            newblob = newcommit.tree.get_blob_by_path(copied['new'])
534
            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
535
            totlines+=_computeLines(newblob, oldblob)
536
537
        for added in d.added:
538
            newblob = newcommit.tree.get_blob_by_path(added)
539
            totlines+=_computeLines(newblob)
540
541
        _addCommitData(self, topics, languages, totlines)
542
543
        self.lastmonth.commits.append(dict(
544
            datetime=commit_datetime, 
545
            categories=topics, 
546
            programming_languages=languages,
547
            lines=totlines))
548
        self.checkOldArtifacts()
549
550
    def _updateArtifactsStats(self, art_type, art_datetime, project, action):
551
        if action not in ['created', 'modified']: 
552
            return
553
        topics = [t for t in project.trove_topic if t]
554
        lt = [None] + topics
555
        for mtype in [None, art_type]:
556
            for t in lt:
557
                i = getElementIndex(self.general, category = t)
558
                if i is None:
559
                    msg = dict(
560
                        category=t,
561
                        commits=[],
562
                        tickets=dict(
563
                            solved=0,
564
                            assigned=0,
565
                            revoked=0,
566
                            totsolvingtime=0),
567
                        messages=[])
568
                    self.general.append(msg)
569
                    i = getElementIndex(self.general, category = t)
570
                j = getElementIndex(
571
                    self.general[i]['messages'], messagetype=mtype)
572
                if j is None:
573
                    entry = dict(messagetype=mtype, created=0, modified=0)
574
                    entry[action] += 1
575
                    self.general[i]['messages'].append(entry)
576
                else:
577
                    self.general[i]['messages'][j][action] += 1
578
579
        self.lastmonth.messages.append(dict(
580
            datetime=art_datetime,
581
            created=(action == 'created'),
582
            categories=topics,
583
            messagetype=art_type))
584
        self.checkOldArtifacts() 
585
586
    def _updateTicketsStats(self, topics, action, s_time = None):
587
        if action not in ['solved', 'assigned', 'revoked']:
588
            return
589
        lt = topics + [None]
590
        for t in lt:
591
            i = getElementIndex(self.general, category = t)
592
            if i is None:
593
                stats = dict(
594
                    category=t,
595
                    commits=[],
596
                    tickets=dict(
597
                        solved=0,
598
                        assigned=0,
599
                        revoked=0,
600
                        totsolvingtime=0),
601
                    messages=[])
602
                self.general.append(stats)
603
                i = getElementIndex(self.general, category = t)
604
            self.general[i]['tickets'][action] += 1 
605
            if action == 'solved': 
606
                self.general[i]['tickets']['totsolvingtime']+=s_time
607
608
def getElementIndex(el_list, **kw):
609
    for i in range(len(el_list)):
610
        for k in kw:
611
            if el_list[i].get(k) != kw[k]:
612
                break
613
        else:
614
            return i
615
    return None
616
617
def addtuple(l1, l2):
618
    a, b = l1
619
    x, y = l2
620
    return (a+x, b+y)
621
622
def _convertTimeDiff(int_seconds):
623
    if int_seconds is None:
624
        return None
625
    diff = timedelta(seconds = int_seconds)
626
    days, seconds = diff.days, diff.seconds
627
    hours = seconds / 3600
628
    seconds = seconds % 3600
629
    minutes = seconds / 60
630
    seconds = seconds % 60
631
    return dict(
632
        days=days, 
633
        hours=hours, 
634
        minutes=minutes,
635
        seconds=seconds)
636
637
Mapper.compile_all()