allura
Revisión | 6f8e88b094634f1992a9412c5f56a720425d7104 (tree) |
---|---|
Tiempo | 2012-04-19 03:33:45 |
Autor | Tim Van Steenburgh <tvansteenburgh@geek...> |
Commiter | Tim Van Steenburgh |
Merge branch 'dev' of git://sfi-engr-scm-1/forge into dev
@@ -1,10 +1,10 @@ | ||
1 | 1 | import re |
2 | 2 | import logging |
3 | - | |
4 | -from bson import ObjectId | |
3 | +from datetime import datetime, timedelta | |
5 | 4 | from urllib import unquote |
6 | 5 | from itertools import chain, islice |
7 | 6 | |
7 | +from bson import ObjectId | |
8 | 8 | from tg import expose, flash, redirect, validate, request, response |
9 | 9 | from tg.decorators import with_trailing_slash, without_trailing_slash |
10 | 10 | from pylons import c, g |
@@ -400,17 +400,19 @@ class NeighborhoodAdminController(object): | ||
400 | 400 | def __init__(self, neighborhood): |
401 | 401 | self.neighborhood = neighborhood |
402 | 402 | self.awards = NeighborhoodAwardsController(self.neighborhood) |
403 | + self.stats = NeighborhoodStatsController(self.neighborhood) | |
404 | + | |
405 | + def _check_security(self): | |
406 | + require_access(self.neighborhood, 'admin') | |
403 | 407 | |
404 | 408 | @with_trailing_slash |
405 | 409 | @expose() |
406 | 410 | def index(self, **kw): |
407 | - require_access(self.neighborhood, 'admin') | |
408 | 411 | utils.permanent_redirect('overview') |
409 | 412 | |
410 | 413 | @without_trailing_slash |
411 | 414 | @expose('jinja:allura:templates/neighborhood_admin_overview.html') |
412 | 415 | def overview(self, **kw): |
413 | - require_access(self.neighborhood, 'admin') | |
414 | 416 | set_nav(self.neighborhood) |
415 | 417 | c.overview_form = W.neighborhood_overview_form |
416 | 418 | return dict(neighborhood=self.neighborhood) |
@@ -418,13 +420,11 @@ class NeighborhoodAdminController(object): | ||
418 | 420 | @without_trailing_slash |
419 | 421 | @expose('jinja:allura:templates/neighborhood_admin_permissions.html') |
420 | 422 | def permissions(self): |
421 | - require_access(self.neighborhood, 'admin') | |
422 | 423 | set_nav(self.neighborhood) |
423 | 424 | return dict(neighborhood=self.neighborhood) |
424 | 425 | |
425 | 426 | @expose('json:') |
426 | 427 | def project_search(self, term=''): |
427 | - require_access(self.neighborhood, 'admin') | |
428 | 428 | if len(term) < 3: |
429 | 429 | raise exc.HTTPBadRequest('"term" param must be at least length 3') |
430 | 430 | project_regex = re.compile('(?i)%s' % re.escape(term)) |
@@ -442,7 +442,6 @@ class NeighborhoodAdminController(object): | ||
442 | 442 | @without_trailing_slash |
443 | 443 | @expose('jinja:allura:templates/neighborhood_admin_accolades.html') |
444 | 444 | def accolades(self): |
445 | - require_access(self.neighborhood, 'admin') | |
446 | 445 | set_nav(self.neighborhood) |
447 | 446 | awards = M.Award.query.find(dict(created_by_neighborhood_id=self.neighborhood._id)).all() |
448 | 447 | awards_count = len(awards) |
@@ -460,7 +459,6 @@ class NeighborhoodAdminController(object): | ||
460 | 459 | @require_post() |
461 | 460 | @validate(W.neighborhood_overview_form, error_handler=overview) |
462 | 461 | def update(self, name=None, css=None, homepage=None, project_template=None, icon=None, **kw): |
463 | - require_access(self.neighborhood, 'admin') | |
464 | 462 | self.neighborhood.name = name |
465 | 463 | self.neighborhood.redirect = kw.pop('redirect', '') |
466 | 464 | self.neighborhood.homepage = homepage |
@@ -485,6 +483,77 @@ class NeighborhoodAdminController(object): | ||
485 | 483 | neighborhood=self.neighborhood, |
486 | 484 | ) |
487 | 485 | |
486 | +class NeighborhoodStatsController(object): | |
487 | + | |
488 | + def __init__(self, neighborhood): | |
489 | + self.neighborhood = neighborhood | |
490 | + | |
491 | + @with_trailing_slash | |
492 | + @expose('jinja:allura:templates/neighborhood_stats.html') | |
493 | + def index(self, **kw): | |
494 | + delete_count = M.Project.query.find(dict(neighborhood_id=self.neighborhood._id, deleted=True)).count() | |
495 | + public_count = 0 | |
496 | + private_count = 0 | |
497 | + last_updated_30 = 0 | |
498 | + last_updated_60 = 0 | |
499 | + last_updated_90 = 0 | |
500 | + today_date = datetime.today() | |
501 | + if M.Project.query.find(dict(neighborhood_id=self.neighborhood._id, deleted=False)).count() < 20000: # arbitrary limit for efficiency | |
502 | + for p in M.Project.query.find(dict(neighborhood_id=self.neighborhood._id, deleted=False)): | |
503 | + if p.private: | |
504 | + private_count = private_count + 1 | |
505 | + else: | |
506 | + public_count = public_count + 1 | |
507 | + if today_date - p.last_updated < timedelta(days=30): | |
508 | + last_updated_30 = last_updated_30 + 1 | |
509 | + if today_date - p.last_updated < timedelta(days=60): | |
510 | + last_updated_60 = last_updated_60 + 1 | |
511 | + if today_date - p.last_updated < timedelta(days=90): | |
512 | + last_updated_90 = last_updated_90 + 1 | |
513 | + | |
514 | + set_nav(self.neighborhood) | |
515 | + return dict( | |
516 | + delete_count = delete_count, | |
517 | + public_count = public_count, | |
518 | + private_count = private_count, | |
519 | + last_updated_30 = last_updated_30, | |
520 | + last_updated_60 = last_updated_60, | |
521 | + last_updated_90 = last_updated_90, | |
522 | + neighborhood = self.neighborhood, | |
523 | + ) | |
524 | + | |
525 | + @without_trailing_slash | |
526 | + @expose('jinja:allura:templates/neighborhood_stats_adminlist.html') | |
527 | + def adminlist(self, sort='alpha', limit=25, page=0, **kw): | |
528 | + limit, page, start = g.handle_paging(limit, page) | |
529 | + | |
530 | + pq = M.Project.query.find(dict(neighborhood_id=self.neighborhood._id, deleted=False)) | |
531 | + if sort=='alpha': | |
532 | + pq.sort('name') | |
533 | + else: | |
534 | + pq.sort('last_updated', pymongo.DESCENDING) | |
535 | + count = pq.count() | |
536 | + projects = pq.skip(start).limit(int(limit)).all() | |
537 | + | |
538 | + entries = [] | |
539 | + for proj in projects: | |
540 | + admin_role = M.ProjectRole.query.get(project_id=proj.root_project._id,name='Admin') | |
541 | + if admin_role is None: | |
542 | + continue | |
543 | + user_role_list = M.ProjectRole.query.find(dict(project_id=proj.root_project._id, name=None)).all() | |
544 | + for ur in user_role_list: | |
545 | + if ur.user is not None and admin_role._id in ur.roles: | |
546 | + entries.append({'project': proj, 'user': ur.user}) | |
547 | + | |
548 | + set_nav(self.neighborhood) | |
549 | + return dict(entries=entries, | |
550 | + sort=sort, | |
551 | + limit=limit, page=page, count=count, | |
552 | + page_list=W.page_list, | |
553 | + neighborhood=self.neighborhood, | |
554 | + ) | |
555 | + | |
556 | + | |
488 | 557 | class NeighborhoodModerateController(object): |
489 | 558 | |
490 | 559 | def __init__(self, neighborhood): |
@@ -1,7 +1,9 @@ | ||
1 | 1 | import logging |
2 | 2 | from collections import defaultdict |
3 | -from datetime import datetime | |
3 | +from datetime import datetime, timedelta | |
4 | 4 | |
5 | +import Image | |
6 | +import pymongo | |
5 | 7 | import pkg_resources |
6 | 8 | from pylons import c, g, request |
7 | 9 | from paste.deploy.converters import asbool |
@@ -40,6 +42,7 @@ class W: | ||
40 | 42 | screenshot_list = ProjectScreenshots() |
41 | 43 | metadata_admin = aw.MetadataAdmin() |
42 | 44 | audit = aw.AuditLog() |
45 | + page_list=ffw.PageList() | |
43 | 46 | |
44 | 47 | class AdminWidgets(WidgetController): |
45 | 48 | widgets=['users', 'tool_status'] |
@@ -138,6 +141,7 @@ class AdminApp(Application): | ||
138 | 141 | links.append(SitemapEntry('Invitation(s)', admin_url+'invitations')) |
139 | 142 | links.append(SitemapEntry('Audit Trail', admin_url+ 'audit/')) |
140 | 143 | if c.project.shortname == '--init--': |
144 | + links.append(SitemapEntry('Statistics', nbhd_admin_url+ 'stats/')) | |
141 | 145 | links.append(None) |
142 | 146 | links.append(SitemapEntry('Help', nbhd_admin_url+ 'help/')) |
143 | 147 | return links |
@@ -728,6 +732,7 @@ class AuditController(BaseController): | ||
728 | 732 | page=page, |
729 | 733 | count=count) |
730 | 734 | |
735 | + | |
731 | 736 | class AdminAppAdminController(DefaultAdminController): |
732 | 737 | '''Administer the admin app''' |
733 | 738 | pass |
@@ -26,6 +26,7 @@ from tg.decorators import before_validate | ||
26 | 26 | from formencode.variabledecode import variable_decode |
27 | 27 | import formencode |
28 | 28 | from jinja2 import Markup |
29 | +from paste.deploy.converters import asbool | |
29 | 30 | |
30 | 31 | from webhelpers import date, feedgenerator, html, number, misc, text |
31 | 32 |
@@ -0,0 +1,33 @@ | ||
1 | +{% extends g.theme.master %} | |
2 | + | |
3 | +{% block top_nav %} | |
4 | + {% include 'allura:templates/jinja_master/neigh_top_nav.html' %} | |
5 | +{% endblock %} | |
6 | + | |
7 | +{% block title %}Neighborhood Statistics{% endblock %} | |
8 | + | |
9 | +{% block header %}Neighborhood Statistics{% endblock %} | |
10 | + | |
11 | +{% block content %} | |
12 | + <div class="grid-9"> | |
13 | + <b>Number of projects that are...</b> | |
14 | + <ul> | |
15 | + <li>Public: {{ public_count }}</li> | |
16 | + <li>Private: {{ private_count }}</li> | |
17 | + {% if h.asbool(config.get('allow_project_delete', True)) %} | |
18 | + <li>Deleted: {{ delete_count }}</li> | |
19 | + {% endif %} | |
20 | + </ul> | |
21 | + </div> | |
22 | + <div class="grid-10"> | |
23 | + <b>Number of projects updated in the last...</b> | |
24 | + <ul> | |
25 | + <li>30 days: {{ last_updated_30 }}</li> | |
26 | + <li>60 days: {{ last_updated_60 }}</li> | |
27 | + <li>90 days: {{ last_updated_90 }}</li> | |
28 | + </ul> | |
29 | + </div> | |
30 | + <div class="grid-19"> | |
31 | + <a href="adminlist">View a list of project admins</a> | |
32 | + </div> | |
33 | +{% endblock %} |
@@ -0,0 +1,37 @@ | ||
1 | +{% extends g.theme.master %} | |
2 | + | |
3 | +{% block title %}{{c.project.name}} / Statistics / Admins list{% endblock %} | |
4 | + | |
5 | +{% block header %}Admins list{% endblock %} | |
6 | + | |
7 | +{% block top_nav %} | |
8 | + {% include 'allura:templates/jinja_master/neigh_top_nav.html' %} | |
9 | +{% endblock %} | |
10 | + | |
11 | +{% block content %} | |
12 | +<table> | |
13 | + <thead> | |
14 | + <tr> | |
15 | + <th>Project</th> | |
16 | + <th>Username</th> | |
17 | + <th>E-mail</th> | |
18 | + <th>Name</th> | |
19 | + </tr> | |
20 | + </thead> | |
21 | + <tbody> | |
22 | + {% for entry in entries %} | |
23 | + <tr> | |
24 | + <td style="white-space: nowrap"><a href="{{entry.project.url()}}">{{ entry.project.name }}</a></td> | |
25 | + <td>{{ entry.user.username }}</td> | |
26 | + <td>{{ entry.user.preferences.email_address or 'Unknown' }}</td> | |
27 | + <td>{{ entry.user.display_name }}</td> | |
28 | + </tr> | |
29 | + {% endfor %} | |
30 | + </tbody> | |
31 | +</table> | |
32 | + | |
33 | +<div class="grid-15" style="clear:both"> | |
34 | + {{page_list.display(limit=limit, page=page, count=count)}} | |
35 | +</div> | |
36 | + | |
37 | +{% endblock %} |
@@ -2,10 +2,12 @@ import json | ||
2 | 2 | import os |
3 | 3 | from cStringIO import StringIO |
4 | 4 | from nose.tools import assert_raises |
5 | +from datetime import datetime, timedelta | |
5 | 6 | |
6 | 7 | import Image |
7 | 8 | from tg import config |
8 | 9 | from nose.tools import assert_equal |
10 | +from ming.orm.ormsession import ThreadLocalORMSession | |
9 | 11 | |
10 | 12 | import allura |
11 | 13 | from allura import model as M |
@@ -44,6 +46,44 @@ class TestNeighborhood(TestController): | ||
44 | 46 | params=dict(project_template='{'), |
45 | 47 | extra_environ=dict(username='root')) |
46 | 48 | assert 'Invalid JSON' in r |
49 | + | |
50 | + def test_admin_stats_del_count(self): | |
51 | + neighborhood = M.Neighborhood.query.get(name='Adobe') | |
52 | + proj = M.Project.query.get(neighborhood_id=neighborhood._id) | |
53 | + proj.deleted = True | |
54 | + ThreadLocalORMSession.flush_all() | |
55 | + r = self.app.get('/adobe/_admin/stats/', extra_environ=dict(username='root')) | |
56 | + assert 'Deleted: 1' in r | |
57 | + assert 'Private: 0' in r | |
58 | + | |
59 | + def test_admin_stats_priv_count(self): | |
60 | + neighborhood = M.Neighborhood.query.get(name='Adobe') | |
61 | + proj = M.Project.query.get(neighborhood_id=neighborhood._id) | |
62 | + proj.deleted = False | |
63 | + proj.private = True | |
64 | + ThreadLocalORMSession.flush_all() | |
65 | + r = self.app.get('/adobe/_admin/stats/', extra_environ=dict(username='root')) | |
66 | + assert 'Deleted: 0' in r | |
67 | + assert 'Private: 1' in r | |
68 | + | |
69 | + def test_admin_stats_adminlist(self): | |
70 | + neighborhood = M.Neighborhood.query.get(name='Adobe') | |
71 | + proj = M.Project.query.get(neighborhood_id=neighborhood._id) | |
72 | + proj.private = False | |
73 | + ThreadLocalORMSession.flush_all() | |
74 | + r = self.app.get('/adobe/_admin/stats/adminlist', extra_environ=dict(username='root')) | |
75 | + pq = M.Project.query.find(dict(neighborhood_id=neighborhood._id, deleted=False)) | |
76 | + pq.sort('name') | |
77 | + projects = pq.skip(0).limit(int(25)).all() | |
78 | + for proj in projects: | |
79 | + admin_role = M.ProjectRole.query.get(project_id=proj.root_project._id,name='Admin') | |
80 | + if admin_role is None: | |
81 | + continue | |
82 | + user_role_list = M.ProjectRole.query.find(dict(project_id=proj.root_project._id, name=None)).all() | |
83 | + for ur in user_role_list: | |
84 | + if ur.user is not None and admin_role._id in ur.roles: | |
85 | + assert proj.name in r | |
86 | + assert ur.user.username in r | |
47 | 87 | |
48 | 88 | def test_icon(self): |
49 | 89 | file_name = 'neo-icon-set-454545-256x350.png' |
@@ -532,4 +572,4 @@ class TestNeighborhood(TestController): | ||
532 | 572 | |
533 | 573 | def test_help(self): |
534 | 574 | r = self.app.get('/p/_admin/help/', extra_environ=dict(username='root')) |
535 | - assert 'macro' in r | |
\ No newline at end of file | ||
575 | + assert 'macro' in r |