##// END OF EJS Templates
implemented basic autoupdating statistics fetched from database
marcink -
r493:2256c78a celery
parent child Browse files
Show More
@@ -1,42 +1,74
1 # List of modules to import when celery starts.
1 # List of modules to import when celery starts.
2 import sys
2 import sys
3 import os
3 import os
4 import ConfigParser
4 import ConfigParser
5 root = os.getcwd()
5 root = os.getcwd()
6
6
7 PYLONS_CONFIG_NAME = 'development.ini'
7 PYLONS_CONFIG_NAME = 'development.ini'
8
8
9 sys.path.append(root)
9 sys.path.append(root)
10 config = ConfigParser.ConfigParser({'here':root})
10 config = ConfigParser.ConfigParser({'here':root})
11 config.read('%s/%s' % (root, PYLONS_CONFIG_NAME))
11 config.read('%s/%s' % (root, PYLONS_CONFIG_NAME))
12 PYLONS_CONFIG = config
12 PYLONS_CONFIG = config
13
13
14 CELERY_IMPORTS = ("pylons_app.lib.celerylib.tasks",)
14 CELERY_IMPORTS = ("pylons_app.lib.celerylib.tasks",)
15
15
16 ## Result store settings.
16 ## Result store settings.
17 CELERY_RESULT_BACKEND = "database"
17 CELERY_RESULT_BACKEND = "database"
18 CELERY_RESULT_DBURI = dict(config.items('app:main'))['sqlalchemy.db1.url']
18 CELERY_RESULT_DBURI = dict(config.items('app:main'))['sqlalchemy.db1.url']
19 CELERY_RESULT_SERIALIZER = 'json'
20
19
21
20 BROKER_CONNECTION_MAX_RETRIES = 30
22 BROKER_CONNECTION_MAX_RETRIES = 30
21
23
22 ## Broker settings.
24 ## Broker settings.
23 BROKER_HOST = "localhost"
25 BROKER_HOST = "localhost"
24 BROKER_PORT = 5672
26 BROKER_PORT = 5672
25 BROKER_VHOST = "rabbitmqhost"
27 BROKER_VHOST = "rabbitmqhost"
26 BROKER_USER = "rabbitmq"
28 BROKER_USER = "rabbitmq"
27 BROKER_PASSWORD = "qweqwe"
29 BROKER_PASSWORD = "qweqwe"
28
30
29 ## Worker settings
31 ## Worker settings
30 ## If you're doing mostly I/O you can have more processes,
32 ## If you're doing mostly I/O you can have more processes,
31 ## but if mostly spending CPU, try to keep it close to the
33 ## but if mostly spending CPU, try to keep it close to the
32 ## number of CPUs on your machine. If not set, the number of CPUs/cores
34 ## number of CPUs on your machine. If not set, the number of CPUs/cores
33 ## available will be used.
35 ## available will be used.
34 CELERYD_CONCURRENCY = 2
36 CELERYD_CONCURRENCY = 2
35 # CELERYD_LOG_FILE = "celeryd.log"
37 # CELERYD_LOG_FILE = "celeryd.log"
36 CELERYD_LOG_LEVEL = "DEBUG"
38 CELERYD_LOG_LEVEL = "DEBUG"
37 CELERYD_MAX_TASKS_PER_CHILD = 1
39 CELERYD_MAX_TASKS_PER_CHILD = 1
38
40
39 #CELERY_ALWAYS_EAGER = True
41 #Tasks will never be sent to the queue, but executed locally instead.
40 #rabbitmqctl add_user rabbitmq qweqwe
42 CELERY_ALWAYS_EAGER = False
41 #rabbitmqctl add_vhost rabbitmqhost
43
42 #rabbitmqctl set_permissions -p rabbitmqhost rabbitmq ".*" ".*" ".*"
44 #===============================================================================
45 # EMAIL SETTINGS
46 #===============================================================================
47 pylons_email_config = dict(config.items('DEFAULT'))
48
49 CELERY_SEND_TASK_ERROR_EMAILS = True
50
51 #List of (name, email_address) tuples for the admins that should receive error e-mails.
52 ADMINS = [('Administrator', pylons_email_config.get('email_to'))]
53
54 #The e-mail address this worker sends e-mails from. Default is "celery@localhost".
55 SERVER_EMAIL = pylons_email_config.get('error_email_from')
56
57 #The mail server to use. Default is "localhost".
58 MAIL_HOST = pylons_email_config.get('smtp_server')
59
60 #Username (if required) to log on to the mail server with.
61 MAIL_HOST_USER = pylons_email_config.get('smtp_username')
62
63 #Password (if required) to log on to the mail server with.
64 MAIL_HOST_PASSWORD = pylons_email_config.get('smtp_password')
65
66 MAIL_PORT = pylons_email_config.get('smtp_port')
67
68
69 #===============================================================================
70 # INSTRUCTIONS FOR RABBITMQ
71 #===============================================================================
72 # rabbitmqctl add_user rabbitmq qweqwe
73 # rabbitmqctl add_vhost rabbitmqhost
74 # rabbitmqctl set_permissions -p rabbitmqhost rabbitmq ".*" ".*" ".*"
@@ -1,71 +1,96
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 # summary controller for pylons
3 # summary controller for pylons
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
9 # of the License or (at your opinion) any later version of the license.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
19 # MA 02110-1301, USA.
20 """
20 """
21 Created on April 18, 2010
21 Created on April 18, 2010
22 summary controller for pylons
22 summary controller for pylons
23 @author: marcink
23 @author: marcink
24 """
24 """
25 from pylons import tmpl_context as c, request, url
25 from pylons import tmpl_context as c, request, url
26 from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
26 from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from pylons_app.lib.base import BaseController, render
27 from pylons_app.lib.base import BaseController, render
28 from pylons_app.lib.utils import OrderedDict
28 from pylons_app.lib.utils import OrderedDict
29 from pylons_app.model.hg_model import HgModel
29 from pylons_app.model.hg_model import HgModel
30 from pylons_app.model.db import Statistics
30 from webhelpers.paginate import Page
31 from webhelpers.paginate import Page
31 from pylons_app.lib.celerylib import run_task
32 from pylons_app.lib.celerylib import run_task
32 from pylons_app.lib.celerylib.tasks import get_commits_stats
33 from pylons_app.lib.celerylib.tasks import get_commits_stats
34 from datetime import datetime, timedelta
35 from time import mktime
36 import calendar
33 import logging
37 import logging
34
38
35 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
36
40
37 class SummaryController(BaseController):
41 class SummaryController(BaseController):
38
42
39 @LoginRequired()
43 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 'repository.admin')
45 'repository.admin')
42 def __before__(self):
46 def __before__(self):
43 super(SummaryController, self).__before__()
47 super(SummaryController, self).__before__()
44
48
45 def index(self):
49 def index(self):
46 hg_model = HgModel()
50 hg_model = HgModel()
47 c.repo_info = hg_model.get_repo(c.repo_name)
51 c.repo_info = hg_model.get_repo(c.repo_name)
48 c.repo_changesets = Page(list(c.repo_info[:10]), page=1, items_per_page=20)
52 c.repo_changesets = Page(list(c.repo_info[:10]), page=1, items_per_page=20)
49 e = request.environ
53 e = request.environ
50 uri = u'%(protocol)s://%(user)s@%(host)s/%(repo_name)s' % {
54 uri = u'%(protocol)s://%(user)s@%(host)s/%(repo_name)s' % {
51 'protocol': e.get('wsgi.url_scheme'),
55 'protocol': e.get('wsgi.url_scheme'),
52 'user':str(c.hg_app_user.username),
56 'user':str(c.hg_app_user.username),
53 'host':e.get('HTTP_HOST'),
57 'host':e.get('HTTP_HOST'),
54 'repo_name':c.repo_name, }
58 'repo_name':c.repo_name, }
55 c.clone_repo_url = uri
59 c.clone_repo_url = uri
56 c.repo_tags = OrderedDict()
60 c.repo_tags = OrderedDict()
57 for name, hash in c.repo_info.tags.items()[:10]:
61 for name, hash in c.repo_info.tags.items()[:10]:
58 c.repo_tags[name] = c.repo_info.get_changeset(hash)
62 c.repo_tags[name] = c.repo_info.get_changeset(hash)
59
63
60 c.repo_branches = OrderedDict()
64 c.repo_branches = OrderedDict()
61 for name, hash in c.repo_info.branches.items()[:10]:
65 for name, hash in c.repo_info.branches.items()[:10]:
62 c.repo_branches[name] = c.repo_info.get_changeset(hash)
66 c.repo_branches[name] = c.repo_info.get_changeset(hash)
63
67
64 task = run_task(get_commits_stats, c.repo_info.name)
68 td = datetime.today() + timedelta(days=1)
65 c.ts_min = task.result[0]
69 y, m, d = td.year, td.month, td.day
66 c.ts_max = task.result[1]
70
67 c.commit_data = task.result[2]
71 ts_min_y = mktime((y - 1, (td - timedelta(days=calendar.mdays[m])).month,
68 c.overview_data = task.result[3]
72 d, 0, 0, 0, 0, 0, 0,))
73 ts_min_m = mktime((y, (td - timedelta(days=calendar.mdays[m])).month,
74 d, 0, 0, 0, 0, 0, 0,))
75
76 ts_max_y = mktime((y, m, d, 0, 0, 0, 0, 0, 0,))
77
78 run_task(get_commits_stats, c.repo_info.name, ts_min_y, ts_max_y)
79 c.ts_min = ts_min_m
80 c.ts_max = ts_max_y
81
82
83 stats = self.sa.query(Statistics)\
84 .filter(Statistics.repository == c.repo_info.dbrepo)\
85 .scalar()
86
87 if stats:
88 c.commit_data = stats.commit_activity
89 c.overview_data = stats.commit_activity_combined
90 else:
91 import json
92 c.commit_data = json.dumps({})
93 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 0] ])
69
94
70 return render('summary/summary.html')
95 return render('summary/summary.html')
71
96
@@ -1,229 +1,266
1 from celery.decorators import task
1 from celery.decorators import task
2 from celery.task.sets import subtask
2 from celery.task.sets import subtask
3 from celeryconfig import PYLONS_CONFIG as config
3 from celeryconfig import PYLONS_CONFIG as config
4 from datetime import datetime, timedelta
5 from pylons.i18n.translation import _
4 from pylons.i18n.translation import _
6 from pylons_app.lib.celerylib import run_task
5 from pylons_app.lib.celerylib import run_task
7 from pylons_app.lib.helpers import person
6 from pylons_app.lib.helpers import person
8 from pylons_app.lib.smtp_mailer import SmtpMailer
7 from pylons_app.lib.smtp_mailer import SmtpMailer
9 from pylons_app.lib.utils import OrderedDict
8 from pylons_app.lib.utils import OrderedDict
10 from operator import itemgetter
9 from operator import itemgetter
11 from vcs.backends.hg import MercurialRepository
10 from vcs.backends.hg import MercurialRepository
12 from time import mktime
11 from time import mktime
13 import calendar
14 import traceback
12 import traceback
15 import json
13 import json
16
14
17 __all__ = ['whoosh_index', 'get_commits_stats',
15 __all__ = ['whoosh_index', 'get_commits_stats',
18 'reset_user_password', 'send_email']
16 'reset_user_password', 'send_email']
19
17
20 def get_session():
18 def get_session():
21 from sqlalchemy import engine_from_config
19 from sqlalchemy import engine_from_config
22 from sqlalchemy.orm import sessionmaker, scoped_session
20 from sqlalchemy.orm import sessionmaker, scoped_session
23 engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.')
21 engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.')
24 sa = scoped_session(sessionmaker(bind=engine))
22 sa = scoped_session(sessionmaker(bind=engine))
25 return sa
23 return sa
26
24
27 def get_hg_settings():
25 def get_hg_settings():
28 from pylons_app.model.db import HgAppSettings
26 from pylons_app.model.db import HgAppSettings
29 try:
27 try:
30 sa = get_session()
28 sa = get_session()
31 ret = sa.query(HgAppSettings).all()
29 ret = sa.query(HgAppSettings).all()
32 finally:
30 finally:
33 sa.remove()
31 sa.remove()
34
32
35 if not ret:
33 if not ret:
36 raise Exception('Could not get application settings !')
34 raise Exception('Could not get application settings !')
37 settings = {}
35 settings = {}
38 for each in ret:
36 for each in ret:
39 settings['hg_app_' + each.app_settings_name] = each.app_settings_value
37 settings['hg_app_' + each.app_settings_name] = each.app_settings_value
40
38
41 return settings
39 return settings
42
40
43 def get_hg_ui_settings():
41 def get_hg_ui_settings():
44 from pylons_app.model.db import HgAppUi
42 from pylons_app.model.db import HgAppUi
45 try:
43 try:
46 sa = get_session()
44 sa = get_session()
47 ret = sa.query(HgAppUi).all()
45 ret = sa.query(HgAppUi).all()
48 finally:
46 finally:
49 sa.remove()
47 sa.remove()
50
48
51 if not ret:
49 if not ret:
52 raise Exception('Could not get application ui settings !')
50 raise Exception('Could not get application ui settings !')
53 settings = {}
51 settings = {}
54 for each in ret:
52 for each in ret:
55 k = each.ui_key
53 k = each.ui_key
56 v = each.ui_value
54 v = each.ui_value
57 if k == '/':
55 if k == '/':
58 k = 'root_path'
56 k = 'root_path'
59
57
60 if k.find('.') != -1:
58 if k.find('.') != -1:
61 k = k.replace('.', '_')
59 k = k.replace('.', '_')
62
60
63 if each.ui_section == 'hooks':
61 if each.ui_section == 'hooks':
64 v = each.ui_active
62 v = each.ui_active
65
63
66 settings[each.ui_section + '_' + k] = v
64 settings[each.ui_section + '_' + k] = v
67
65
68 return settings
66 return settings
69
67
70 @task
68 @task
71 def whoosh_index(repo_location, full_index):
69 def whoosh_index(repo_location, full_index):
72 log = whoosh_index.get_logger()
70 log = whoosh_index.get_logger()
73 from pylons_app.lib.indexers import DaemonLock
71 from pylons_app.lib.indexers import DaemonLock
74 from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon, LockHeld
72 from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon, LockHeld
75 try:
73 try:
76 l = DaemonLock()
74 l = DaemonLock()
77 WhooshIndexingDaemon(repo_location=repo_location)\
75 WhooshIndexingDaemon(repo_location=repo_location)\
78 .run(full_index=full_index)
76 .run(full_index=full_index)
79 l.release()
77 l.release()
80 return 'Done'
78 return 'Done'
81 except LockHeld:
79 except LockHeld:
82 log.info('LockHeld')
80 log.info('LockHeld')
83 return 'LockHeld'
81 return 'LockHeld'
84
82
85 @task
83 @task
86 def get_commits_stats(repo):
84 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
85 author_key_cleaner = lambda k: person(k).replace('"', "") #for js data compatibilty
86
87 from pylons_app.model.db import Statistics, Repository
87 log = get_commits_stats.get_logger()
88 log = get_commits_stats.get_logger()
88 aggregate = OrderedDict()
89 commits_by_day_author_aggregate = {}
89 overview_aggregate = OrderedDict()
90 commits_by_day_aggregate = {}
90 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
91 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
91 repo = MercurialRepository(repos_path + repo)
92 repo = MercurialRepository(repos_path + repo_name)
92 #graph range
93
93 td = datetime.today() + timedelta(days=1)
94 skip_date_limit = True
94 y, m, d = td.year, td.month, td.day
95 parse_limit = 500 #limit for single task changeset parsing
96 last_rev = 0
97 last_cs = None
98 timegetter = itemgetter('time')
99
100 sa = get_session()
95
101
96 ts_min_y = mktime((y - 1, (td - timedelta(days=calendar.mdays[m])).month,
102 dbrepo = sa.query(Repository)\
97 d, 0, 0, 0, 0, 0, 0,))
103 .filter(Repository.repo_name == repo_name).scalar()
98 ts_min_m = mktime((y, (td - timedelta(days=calendar.mdays[m])).month,
104 cur_stats = sa.query(Statistics)\
99 d, 0, 0, 0, 0, 0, 0,))
105 .filter(Statistics.repository == dbrepo).scalar()
106 if cur_stats:
107 last_rev = cur_stats.stat_on_revision
100
108
101 ts_max_y = mktime((y, m, d, 0, 0, 0, 0, 0, 0,))
109 if last_rev == repo.revisions[-1]:
102 skip_date_limit = True
110 #pass silently without any work
111 return True
103
112
104 def author_key_cleaner(k):
113 if cur_stats:
105 k = person(k)
114 commits_by_day_aggregate = OrderedDict(
106 k = k.replace('"', "") #for js data compatibilty
115 json.loads(
107 return k
116 cur_stats.commit_activity_combined))
108
117 commits_by_day_author_aggregate = json.loads(cur_stats.commit_activity)
109 for cs in repo[:200]:#added limit 200 until fix #29 is made
118
119 for cnt, rev in enumerate(repo.revisions[last_rev:]):
120 last_cs = cs = repo.get_changeset(rev)
110 k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1],
121 k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1],
111 cs.date.timetuple()[2])
122 cs.date.timetuple()[2])
112 timetupple = [int(x) for x in k.split('-')]
123 timetupple = [int(x) for x in k.split('-')]
113 timetupple.extend([0 for _ in xrange(6)])
124 timetupple.extend([0 for _ in xrange(6)])
114 k = mktime(timetupple)
125 k = mktime(timetupple)
115 if aggregate.has_key(author_key_cleaner(cs.author)):
126 if commits_by_day_author_aggregate.has_key(author_key_cleaner(cs.author)):
116 if aggregate[author_key_cleaner(cs.author)].has_key(k):
127 try:
117 aggregate[author_key_cleaner(cs.author)][k]["commits"] += 1
128 l = [timegetter(x) for x in commits_by_day_author_aggregate\
118 aggregate[author_key_cleaner(cs.author)][k]["added"] += len(cs.added)
129 [author_key_cleaner(cs.author)]['data']]
119 aggregate[author_key_cleaner(cs.author)][k]["changed"] += len(cs.changed)
130 time_pos = l.index(k)
120 aggregate[author_key_cleaner(cs.author)][k]["removed"] += len(cs.removed)
131 except ValueError:
132 time_pos = False
133
134 if time_pos >= 0 and time_pos is not False:
135
136 datadict = commits_by_day_author_aggregate\
137 [author_key_cleaner(cs.author)]['data'][time_pos]
138
139 datadict["commits"] += 1
140 datadict["added"] += len(cs.added)
141 datadict["changed"] += len(cs.changed)
142 datadict["removed"] += len(cs.removed)
143 #print datadict
121
144
122 else:
145 else:
123 #aggregate[author_key_cleaner(cs.author)].update(dates_range)
146 #print 'ELSE !!!!'
124 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
147 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
125 aggregate[author_key_cleaner(cs.author)][k] = {}
148
126 aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1
149 datadict = {"time":k,
127 aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added)
150 "commits":1,
128 aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed)
151 "added":len(cs.added),
129 aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed)
152 "changed":len(cs.changed),
153 "removed":len(cs.removed),
154 }
155 commits_by_day_author_aggregate\
156 [author_key_cleaner(cs.author)]['data'].append(datadict)
130
157
131 else:
158 else:
159 #print k, 'nokey ADDING'
132 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
160 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
133 aggregate[author_key_cleaner(cs.author)] = OrderedDict()
161 commits_by_day_author_aggregate[author_key_cleaner(cs.author)] = {
134 #aggregate[author_key_cleaner(cs.author)].update(dates_range)
162 "label":author_key_cleaner(cs.author),
135 aggregate[author_key_cleaner(cs.author)][k] = {}
163 "data":[{"time":k,
136 aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1
164 "commits":1,
137 aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added)
165 "added":len(cs.added),
138 aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed)
166 "changed":len(cs.changed),
139 aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed)
167 "removed":len(cs.removed),
168 }],
169 "schema":["commits"],
170 }
140
171
141
172 # #gather all data by day
142 if overview_aggregate.has_key(k):
173 if commits_by_day_aggregate.has_key(k):
143 overview_aggregate[k] += 1
174 commits_by_day_aggregate[k] += 1
144 else:
175 else:
145 overview_aggregate[k] = 1
176 commits_by_day_aggregate[k] = 1
146
177
178 if cnt >= parse_limit:
179 #don't fetch to much data since we can freeze application
180 break
181
147 overview_data = []
182 overview_data = []
148 for k, v in overview_aggregate.items():
183 for k, v in commits_by_day_aggregate.items():
149 overview_data.append([k, v])
184 overview_data.append([k, v])
150 overview_data = sorted(overview_data, key=itemgetter(0))
185 overview_data = sorted(overview_data, key=itemgetter(0))
151 data = {}
152 for author in aggregate:
153 commit_data = sorted([{"time":x,
154 "commits":aggregate[author][x]['commits'],
155 "added":aggregate[author][x]['added'],
156 "changed":aggregate[author][x]['changed'],
157 "removed":aggregate[author][x]['removed'],
158 } for x in aggregate[author]],
159 key=itemgetter('time'))
160
186
161 data[author] = {"label":author,
187 if not commits_by_day_author_aggregate:
162 "data":commit_data,
188 commits_by_day_author_aggregate[author_key_cleaner(repo.contact)] = {
163 "schema":["commits"]
164 }
165
166 if not data:
167 data[author_key_cleaner(repo.contact)] = {
168 "label":author_key_cleaner(repo.contact),
189 "label":author_key_cleaner(repo.contact),
169 "data":[0, 1],
190 "data":[0, 1],
170 "schema":["commits"],
191 "schema":["commits"],
171 }
192 }
172
193
173 return (ts_min_m, ts_max_y, json.dumps(data), json.dumps(overview_data))
194 stats = cur_stats if cur_stats else Statistics()
195 stats.commit_activity = json.dumps(commits_by_day_author_aggregate)
196 stats.commit_activity_combined = json.dumps(overview_data)
197 stats.repository = dbrepo
198 stats.stat_on_revision = last_cs.revision
199 stats.languages = json.dumps({'_TOTAL_':0, '':0})
200
201 try:
202 sa.add(stats)
203 sa.commit()
204 except:
205 log.error(traceback.format_exc())
206 sa.rollback()
207 return False
208
209 return True
174
210
175 @task
211 @task
176 def reset_user_password(user_email):
212 def reset_user_password(user_email):
177 log = reset_user_password.get_logger()
213 log = reset_user_password.get_logger()
178 from pylons_app.lib import auth
214 from pylons_app.lib import auth
179 from pylons_app.model.db import User
215 from pylons_app.model.db import User
180
216
181 try:
217 try:
182 try:
218 try:
183 sa = get_session()
219 sa = get_session()
184 user = sa.query(User).filter(User.email == user_email).scalar()
220 user = sa.query(User).filter(User.email == user_email).scalar()
185 new_passwd = auth.PasswordGenerator().gen_password(8,
221 new_passwd = auth.PasswordGenerator().gen_password(8,
186 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
222 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
187 user.password = auth.get_crypt_password(new_passwd)
223 if user:
188 sa.add(user)
224 user.password = auth.get_crypt_password(new_passwd)
189 sa.commit()
225 sa.add(user)
190 log.info('change password for %s', user_email)
226 sa.commit()
227 log.info('change password for %s', user_email)
191 if new_passwd is None:
228 if new_passwd is None:
192 raise Exception('unable to generate new password')
229 raise Exception('unable to generate new password')
193
230
194 except:
231 except:
195 log.error(traceback.format_exc())
232 log.error(traceback.format_exc())
196 sa.rollback()
233 sa.rollback()
197
234
198 run_task(send_email, user_email,
235 run_task(send_email, user_email,
199 "Your new hg-app password",
236 "Your new hg-app password",
200 'Your new hg-app password:%s' % (new_passwd))
237 'Your new hg-app password:%s' % (new_passwd))
201 log.info('send new password mail to %s', user_email)
238 log.info('send new password mail to %s', user_email)
202
239
203
240
204 except:
241 except:
205 log.error('Failed to update user password')
242 log.error('Failed to update user password')
206 log.error(traceback.format_exc())
243 log.error(traceback.format_exc())
207 return True
244 return True
208
245
209 @task
246 @task
210 def send_email(recipients, subject, body):
247 def send_email(recipients, subject, body):
211 log = send_email.get_logger()
248 log = send_email.get_logger()
212 email_config = dict(config.items('DEFAULT'))
249 email_config = dict(config.items('DEFAULT'))
213 mail_from = email_config.get('app_email_from')
250 mail_from = email_config.get('app_email_from')
214 user = email_config.get('smtp_username')
251 user = email_config.get('smtp_username')
215 passwd = email_config.get('smtp_password')
252 passwd = email_config.get('smtp_password')
216 mail_server = email_config.get('smtp_server')
253 mail_server = email_config.get('smtp_server')
217 mail_port = email_config.get('smtp_port')
254 mail_port = email_config.get('smtp_port')
218 tls = email_config.get('smtp_use_tls')
255 tls = email_config.get('smtp_use_tls')
219 ssl = False
256 ssl = False
220
257
221 try:
258 try:
222 m = SmtpMailer(mail_from, user, passwd, mail_server,
259 m = SmtpMailer(mail_from, user, passwd, mail_server,
223 mail_port, ssl, tls)
260 mail_port, ssl, tls)
224 m.send(recipients, subject, body)
261 m.send(recipients, subject, body)
225 except:
262 except:
226 log.error('Mail sending failed')
263 log.error('Mail sending failed')
227 log.error(traceback.format_exc())
264 log.error(traceback.format_exc())
228 return False
265 return False
229 return True
266 return True
@@ -1,125 +1,134
1 from pylons_app.model.meta import Base
1 from pylons_app.model.meta import Base
2 from sqlalchemy import *
2 from sqlalchemy import *
3 from sqlalchemy.orm import relation, backref
3 from sqlalchemy.orm import relation, backref
4 from sqlalchemy.orm.session import Session
4 from sqlalchemy.orm.session import Session
5 from vcs.utils.lazy import LazyProperty
5 from vcs.utils.lazy import LazyProperty
6 import logging
6 import logging
7
7
8 log = logging.getLogger(__name__)
8 log = logging.getLogger(__name__)
9
9
10 class HgAppSettings(Base):
10 class HgAppSettings(Base):
11 __tablename__ = 'hg_app_settings'
11 __tablename__ = 'hg_app_settings'
12 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
12 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
13 app_settings_id = Column("app_settings_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
13 app_settings_id = Column("app_settings_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
14 app_settings_name = Column("app_settings_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
14 app_settings_name = Column("app_settings_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
15 app_settings_value = Column("app_settings_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
15 app_settings_value = Column("app_settings_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
16
16
17 class HgAppUi(Base):
17 class HgAppUi(Base):
18 __tablename__ = 'hg_app_ui'
18 __tablename__ = 'hg_app_ui'
19 __table_args__ = {'useexisting':True}
19 __table_args__ = {'useexisting':True}
20 ui_id = Column("ui_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
20 ui_id = Column("ui_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
21 ui_section = Column("ui_section", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
21 ui_section = Column("ui_section", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
22 ui_key = Column("ui_key", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
22 ui_key = Column("ui_key", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
23 ui_value = Column("ui_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
23 ui_value = Column("ui_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
24 ui_active = Column("ui_active", BOOLEAN(), nullable=True, unique=None, default=True)
24 ui_active = Column("ui_active", BOOLEAN(), nullable=True, unique=None, default=True)
25
25
26
26
27 class User(Base):
27 class User(Base):
28 __tablename__ = 'users'
28 __tablename__ = 'users'
29 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
29 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
30 user_id = Column("user_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
30 user_id = Column("user_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
31 username = Column("username", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
31 username = Column("username", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
32 password = Column("password", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
32 password = Column("password", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
33 active = Column("active", BOOLEAN(), nullable=True, unique=None, default=None)
33 active = Column("active", BOOLEAN(), nullable=True, unique=None, default=None)
34 admin = Column("admin", BOOLEAN(), nullable=True, unique=None, default=False)
34 admin = Column("admin", BOOLEAN(), nullable=True, unique=None, default=False)
35 name = Column("name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
35 name = Column("name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
36 lastname = Column("lastname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
36 lastname = Column("lastname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
37 email = Column("email", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
37 email = Column("email", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
38 last_login = Column("last_login", DATETIME(timezone=False), nullable=True, unique=None, default=None)
38 last_login = Column("last_login", DATETIME(timezone=False), nullable=True, unique=None, default=None)
39
39
40 user_log = relation('UserLog')
40 user_log = relation('UserLog')
41 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id")
41 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id")
42
42
43 @LazyProperty
43 @LazyProperty
44 def full_contact(self):
44 def full_contact(self):
45 return '%s %s <%s>' % (self.name, self.lastname, self.email)
45 return '%s %s <%s>' % (self.name, self.lastname, self.email)
46
46
47 def __repr__(self):
47 def __repr__(self):
48 return "<User('id:%s:%s')>" % (self.user_id, self.username)
48 return "<User('id:%s:%s')>" % (self.user_id, self.username)
49
49
50 def update_lastlogin(self):
50 def update_lastlogin(self):
51 """Update user lastlogin"""
51 """Update user lastlogin"""
52 import datetime
52 import datetime
53
53
54 try:
54 try:
55 session = Session.object_session(self)
55 session = Session.object_session(self)
56 self.last_login = datetime.datetime.now()
56 self.last_login = datetime.datetime.now()
57 session.add(self)
57 session.add(self)
58 session.commit()
58 session.commit()
59 log.debug('updated user %s lastlogin', self.username)
59 log.debug('updated user %s lastlogin', self.username)
60 except Exception:
60 except Exception:
61 session.rollback()
61 session.rollback()
62
62
63
63
64 class UserLog(Base):
64 class UserLog(Base):
65 __tablename__ = 'user_logs'
65 __tablename__ = 'user_logs'
66 __table_args__ = {'useexisting':True}
66 __table_args__ = {'useexisting':True}
67 user_log_id = Column("user_log_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
67 user_log_id = Column("user_log_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
68 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
68 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
69 user_ip = Column("user_ip", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
69 user_ip = Column("user_ip", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
70 repository = Column("repository", TEXT(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_name'), nullable=False, unique=None, default=None)
70 repository = Column("repository", TEXT(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_name'), nullable=False, unique=None, default=None)
71 action = Column("action", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
71 action = Column("action", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
72 action_date = Column("action_date", DATETIME(timezone=False), nullable=True, unique=None, default=None)
72 action_date = Column("action_date", DATETIME(timezone=False), nullable=True, unique=None, default=None)
73
73
74 user = relation('User')
74 user = relation('User')
75
75
76 class Repository(Base):
76 class Repository(Base):
77 __tablename__ = 'repositories'
77 __tablename__ = 'repositories'
78 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
78 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
79 repo_id = Column("repo_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
79 repo_id = Column("repo_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
80 repo_name = Column("repo_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
80 repo_name = Column("repo_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
81 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
81 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
82 private = Column("private", BOOLEAN(), nullable=True, unique=None, default=None)
82 private = Column("private", BOOLEAN(), nullable=True, unique=None, default=None)
83 description = Column("description", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
83 description = Column("description", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
84
84
85 user = relation('User')
85 user = relation('User')
86 repo_to_perm = relation('RepoToPerm', cascade='all')
86 repo_to_perm = relation('RepoToPerm', cascade='all')
87
87
88 def __repr__(self):
88 def __repr__(self):
89 return "<Repository('id:%s:%s')>" % (self.repo_id, self.repo_name)
89 return "<Repository('id:%s:%s')>" % (self.repo_id, self.repo_name)
90
90
91 class Permission(Base):
91 class Permission(Base):
92 __tablename__ = 'permissions'
92 __tablename__ = 'permissions'
93 __table_args__ = {'useexisting':True}
93 __table_args__ = {'useexisting':True}
94 permission_id = Column("permission_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
94 permission_id = Column("permission_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
95 permission_name = Column("permission_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
95 permission_name = Column("permission_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
96 permission_longname = Column("permission_longname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
96 permission_longname = Column("permission_longname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
97
97
98 def __repr__(self):
98 def __repr__(self):
99 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
99 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
100
100
101 class RepoToPerm(Base):
101 class RepoToPerm(Base):
102 __tablename__ = 'repo_to_perm'
102 __tablename__ = 'repo_to_perm'
103 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
103 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
104 repo_to_perm_id = Column("repo_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
104 repo_to_perm_id = Column("repo_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
105 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
105 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
106 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
106 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
107 repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
107 repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
108
108
109 user = relation('User')
109 user = relation('User')
110 permission = relation('Permission')
110 permission = relation('Permission')
111 repository = relation('Repository')
111 repository = relation('Repository')
112
112
113 class UserToPerm(Base):
113 class UserToPerm(Base):
114 __tablename__ = 'user_to_perm'
114 __tablename__ = 'user_to_perm'
115 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
115 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
116 user_to_perm_id = Column("user_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
116 user_to_perm_id = Column("user_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
117 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
117 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
118 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
118 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
119
119
120 user = relation('User')
120 user = relation('User')
121 permission = relation('Permission')
121 permission = relation('Permission')
122
122
123
123 class Statistics(Base):
124 __tablename__ = 'statistics'
125 __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
126 stat_id = Column("stat_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
127 repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
128 stat_on_revision = Column("stat_on_revision", INTEGER(), nullable=False)
129 commit_activity = Column("commit_activity", BLOB(), nullable=False)#JSON data
130 commit_activity_combined = Column("commit_activity_combined", BLOB(), nullable=False)#JSON data
131 languages = Column("languages", BLOB(), nullable=False)#JSON data
132
133 repository = relation('Repository')
124
134
125
@@ -1,508 +1,508
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('Mercurial Repository Overview')}
4 ${_('Mercurial Repository Overview')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(u'Home',h.url('/'))}
8 ${h.link_to(u'Home',h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
10 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 &raquo;
11 &raquo;
12 ${_('summary')}
12 ${_('summary')}
13 </%def>
13 </%def>
14
14
15 <%def name="page_nav()">
15 <%def name="page_nav()">
16 ${self.menu('summary')}
16 ${self.menu('summary')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <script type="text/javascript">
20 <script type="text/javascript">
21 var E = YAHOO.util.Event;
21 var E = YAHOO.util.Event;
22 var D = YAHOO.util.Dom;
22 var D = YAHOO.util.Dom;
23
23
24 E.onDOMReady(function(e){
24 E.onDOMReady(function(e){
25 id = 'clone_url';
25 id = 'clone_url';
26 E.addListener(id,'click',function(e){
26 E.addListener(id,'click',function(e){
27 D.get('clone_url').select();
27 D.get('clone_url').select();
28 })
28 })
29 })
29 })
30 </script>
30 </script>
31 <div class="box box-left">
31 <div class="box box-left">
32 <!-- box / title -->
32 <!-- box / title -->
33 <div class="title">
33 <div class="title">
34 ${self.breadcrumbs()}
34 ${self.breadcrumbs()}
35 </div>
35 </div>
36 <!-- end box / title -->
36 <!-- end box / title -->
37 <div class="form">
37 <div class="form">
38 <div class="fields">
38 <div class="fields">
39
39
40 <div class="field">
40 <div class="field">
41 <div class="label">
41 <div class="label">
42 <label>${_('Name')}:</label>
42 <label>${_('Name')}:</label>
43 </div>
43 </div>
44 <div class="input-short">
44 <div class="input-short">
45 <span style="font-size: 1.6em;font-weight: bold">${c.repo_info.name}</span>
45 <span style="font-size: 1.6em;font-weight: bold">${c.repo_info.name}</span>
46 </div>
46 </div>
47 </div>
47 </div>
48
48
49
49
50 <div class="field">
50 <div class="field">
51 <div class="label">
51 <div class="label">
52 <label>${_('Description')}:</label>
52 <label>${_('Description')}:</label>
53 </div>
53 </div>
54 <div class="input-short">
54 <div class="input-short">
55 ${c.repo_info.description}
55 ${c.repo_info.description}
56 </div>
56 </div>
57 </div>
57 </div>
58
58
59
59
60 <div class="field">
60 <div class="field">
61 <div class="label">
61 <div class="label">
62 <label>${_('Contact')}:</label>
62 <label>${_('Contact')}:</label>
63 </div>
63 </div>
64 <div class="input-short">
64 <div class="input-short">
65 <div class="gravatar">
65 <div class="gravatar">
66 <img alt="gravatar" src="${h.gravatar_url(c.repo_info.dbrepo.user.email)}"/>
66 <img alt="gravatar" src="${h.gravatar_url(c.repo_info.dbrepo.user.email)}"/>
67 </div>
67 </div>
68 ${_('Username')}: ${c.repo_info.dbrepo.user.username}<br/>
68 ${_('Username')}: ${c.repo_info.dbrepo.user.username}<br/>
69 ${_('Name')}: ${c.repo_info.dbrepo.user.name} ${c.repo_info.dbrepo.user.lastname}<br/>
69 ${_('Name')}: ${c.repo_info.dbrepo.user.name} ${c.repo_info.dbrepo.user.lastname}<br/>
70 ${_('Email')}: <a href="mailto:${c.repo_info.dbrepo.user.email}">${c.repo_info.dbrepo.user.email}</a>
70 ${_('Email')}: <a href="mailto:${c.repo_info.dbrepo.user.email}">${c.repo_info.dbrepo.user.email}</a>
71 </div>
71 </div>
72 </div>
72 </div>
73
73
74 <div class="field">
74 <div class="field">
75 <div class="label">
75 <div class="label">
76 <label>${_('Last change')}:</label>
76 <label>${_('Last change')}:</label>
77 </div>
77 </div>
78 <div class="input-short">
78 <div class="input-short">
79 ${h.age(c.repo_info.last_change)} - ${h.rfc822date(c.repo_info.last_change)}
79 ${h.age(c.repo_info.last_change)} - ${h.rfc822date(c.repo_info.last_change)}
80 ${_('by')} ${h.get_changeset_safe(c.repo_info,'tip').author}
80 ${_('by')} ${h.get_changeset_safe(c.repo_info,'tip').author}
81
81
82 </div>
82 </div>
83 </div>
83 </div>
84
84
85 <div class="field">
85 <div class="field">
86 <div class="label">
86 <div class="label">
87 <label>${_('Clone url')}:</label>
87 <label>${_('Clone url')}:</label>
88 </div>
88 </div>
89 <div class="input-short">
89 <div class="input-short">
90 <input type="text" id="clone_url" readonly="readonly" value="hg clone ${c.clone_repo_url}" size="70"/>
90 <input type="text" id="clone_url" readonly="readonly" value="hg clone ${c.clone_repo_url}" size="70"/>
91 </div>
91 </div>
92 </div>
92 </div>
93
93
94 <div class="field">
94 <div class="field">
95 <div class="label">
95 <div class="label">
96 <label>${_('Download')}:</label>
96 <label>${_('Download')}:</label>
97 </div>
97 </div>
98 <div class="input-short">
98 <div class="input-short">
99 %for cnt,archive in enumerate(c.repo_info._get_archives()):
99 %for cnt,archive in enumerate(c.repo_info._get_archives()):
100 %if cnt >=1:
100 %if cnt >=1:
101 |
101 |
102 %endif
102 %endif
103 ${h.link_to(c.repo_info.name+'.'+archive['type'],
103 ${h.link_to(c.repo_info.name+'.'+archive['type'],
104 h.url('files_archive_home',repo_name=c.repo_info.name,
104 h.url('files_archive_home',repo_name=c.repo_info.name,
105 revision='tip',fileformat=archive['extension']),class_="archive_icon")}
105 revision='tip',fileformat=archive['extension']),class_="archive_icon")}
106 %endfor
106 %endfor
107 </div>
107 </div>
108 </div>
108 </div>
109
109
110 <div class="field">
110 <div class="field">
111 <div class="label">
111 <div class="label">
112 <label>${_('Feeds')}:</label>
112 <label>${_('Feeds')}:</label>
113 </div>
113 </div>
114 <div class="input-short">
114 <div class="input-short">
115 ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.repo_info.name),class_='rss_icon')}
115 ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.repo_info.name),class_='rss_icon')}
116 ${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.repo_info.name),class_='atom_icon')}
116 ${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.repo_info.name),class_='atom_icon')}
117 </div>
117 </div>
118 </div>
118 </div>
119 </div>
119 </div>
120 </div>
120 </div>
121 </div>
121 </div>
122
122
123 <div class="box box-right" style="min-height:455px">
123 <div class="box box-right" style="min-height:455px">
124 <!-- box / title -->
124 <!-- box / title -->
125 <div class="title">
125 <div class="title">
126 <h5>${_('Commit activity')}</h5>
126 <h5>${_('Commit activity by day / author')}</h5>
127 </div>
127 </div>
128
128
129 <div class="table">
129 <div class="table">
130 <div id="commit_history" style="width:560px;height:300px;float:left"></div>
130 <div id="commit_history" style="width:560px;height:300px;float:left"></div>
131 <div style="clear: both;height: 10px"></div>
131 <div style="clear: both;height: 10px"></div>
132 <div id="overview" style="width:560px;height:100px;float:left"></div>
132 <div id="overview" style="width:560px;height:100px;float:left"></div>
133
133
134 <div id="legend_data" style="clear:both;margin-top:10px;">
134 <div id="legend_data" style="clear:both;margin-top:10px;">
135 <div id="legend_container"></div>
135 <div id="legend_container"></div>
136 <div id="legend_choices">
136 <div id="legend_choices">
137 <table id="legend_choices_tables" style="font-size:smaller;color:#545454"></table>
137 <table id="legend_choices_tables" style="font-size:smaller;color:#545454"></table>
138 </div>
138 </div>
139 </div>
139 </div>
140 <script type="text/javascript">
140 <script type="text/javascript">
141 /**
141 /**
142 * Plots summary graph
142 * Plots summary graph
143 *
143 *
144 * @class SummaryPlot
144 * @class SummaryPlot
145 * @param {from} initial from for detailed graph
145 * @param {from} initial from for detailed graph
146 * @param {to} initial to for detailed graph
146 * @param {to} initial to for detailed graph
147 * @param {dataset}
147 * @param {dataset}
148 * @param {overview_dataset}
148 * @param {overview_dataset}
149 */
149 */
150 function SummaryPlot(from,to,dataset,overview_dataset) {
150 function SummaryPlot(from,to,dataset,overview_dataset) {
151 var initial_ranges = {
151 var initial_ranges = {
152 "xaxis":{
152 "xaxis":{
153 "from":from,
153 "from":from,
154 "to":to,
154 "to":to,
155 },
155 },
156 };
156 };
157 var dataset = dataset;
157 var dataset = dataset;
158 var overview_dataset = [overview_dataset];
158 var overview_dataset = [overview_dataset];
159 var choiceContainer = YAHOO.util.Dom.get("legend_choices");
159 var choiceContainer = YAHOO.util.Dom.get("legend_choices");
160 var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables");
160 var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables");
161 var plotContainer = YAHOO.util.Dom.get('commit_history');
161 var plotContainer = YAHOO.util.Dom.get('commit_history');
162 var overviewContainer = YAHOO.util.Dom.get('overview');
162 var overviewContainer = YAHOO.util.Dom.get('overview');
163
163
164 var plot_options = {
164 var plot_options = {
165 bars: {show:true,align:'center',lineWidth:4},
165 bars: {show:true,align:'center',lineWidth:4},
166 legend: {show:true, container:"legend_container"},
166 legend: {show:true, container:"legend_container"},
167 points: {show:true,radius:0,fill:false},
167 points: {show:true,radius:0,fill:false},
168 yaxis: {tickDecimals:0,},
168 yaxis: {tickDecimals:0,},
169 xaxis: {
169 xaxis: {
170 mode: "time",
170 mode: "time",
171 timeformat: "%d/%m",
171 timeformat: "%d/%m",
172 min:from,
172 min:from,
173 max:to,
173 max:to,
174 },
174 },
175 grid: {
175 grid: {
176 hoverable: true,
176 hoverable: true,
177 clickable: true,
177 clickable: true,
178 autoHighlight:true,
178 autoHighlight:true,
179 color: "#999"
179 color: "#999"
180 },
180 },
181 //selection: {mode: "x"}
181 //selection: {mode: "x"}
182 };
182 };
183 var overview_options = {
183 var overview_options = {
184 legend:{show:false},
184 legend:{show:false},
185 bars: {show:true,barWidth: 2,},
185 bars: {show:true,barWidth: 2,},
186 shadowSize: 0,
186 shadowSize: 0,
187 xaxis: {mode: "time", timeformat: "%d/%m/%y",},
187 xaxis: {mode: "time", timeformat: "%d/%m/%y",},
188 yaxis: {ticks: 3, min: 0,},
188 yaxis: {ticks: 3, min: 0,},
189 grid: {color: "#999",},
189 grid: {color: "#999",},
190 selection: {mode: "x"}
190 selection: {mode: "x"}
191 };
191 };
192
192
193 /**
193 /**
194 *get dummy data needed in few places
194 *get dummy data needed in few places
195 */
195 */
196 function getDummyData(label){
196 function getDummyData(label){
197 return {"label":label,
197 return {"label":label,
198 "data":[{"time":0,
198 "data":[{"time":0,
199 "commits":0,
199 "commits":0,
200 "added":0,
200 "added":0,
201 "changed":0,
201 "changed":0,
202 "removed":0,
202 "removed":0,
203 }],
203 }],
204 "schema":["commits"],
204 "schema":["commits"],
205 "color":'#ffffff',
205 "color":'#ffffff',
206 }
206 }
207 }
207 }
208
208
209 /**
209 /**
210 * generate checkboxes accordindly to data
210 * generate checkboxes accordindly to data
211 * @param keys
211 * @param keys
212 * @returns
212 * @returns
213 */
213 */
214 function generateCheckboxes(data) {
214 function generateCheckboxes(data) {
215 //append checkboxes
215 //append checkboxes
216 var i = 0;
216 var i = 0;
217 choiceContainerTable.innerHTML = '';
217 choiceContainerTable.innerHTML = '';
218 for(var pos in data) {
218 for(var pos in data) {
219
219
220 data[pos].color = i;
220 data[pos].color = i;
221 i++;
221 i++;
222 if(data[pos].label != ''){
222 if(data[pos].label != ''){
223 choiceContainerTable.innerHTML += '<tr><td>'+
223 choiceContainerTable.innerHTML += '<tr><td>'+
224 '<input type="checkbox" name="' + data[pos].label +'" checked="checked" />'
224 '<input type="checkbox" name="' + data[pos].label +'" checked="checked" />'
225 +data[pos].label+
225 +data[pos].label+
226 '</td></tr>';
226 '</td></tr>';
227 }
227 }
228 }
228 }
229 }
229 }
230
230
231 /**
231 /**
232 * ToolTip show
232 * ToolTip show
233 */
233 */
234 function showTooltip(x, y, contents) {
234 function showTooltip(x, y, contents) {
235 var div=document.getElementById('tooltip');
235 var div=document.getElementById('tooltip');
236 if(!div) {
236 if(!div) {
237 div = document.createElement('div');
237 div = document.createElement('div');
238 div.id="tooltip";
238 div.id="tooltip";
239 div.style.position="absolute";
239 div.style.position="absolute";
240 div.style.border='1px solid #fdd';
240 div.style.border='1px solid #fdd';
241 div.style.padding='2px';
241 div.style.padding='2px';
242 div.style.backgroundColor='#fee';
242 div.style.backgroundColor='#fee';
243 document.body.appendChild(div);
243 document.body.appendChild(div);
244 }
244 }
245 YAHOO.util.Dom.setStyle(div, 'opacity', 0);
245 YAHOO.util.Dom.setStyle(div, 'opacity', 0);
246 div.innerHTML = contents;
246 div.innerHTML = contents;
247 div.style.top=(y + 5) + "px";
247 div.style.top=(y + 5) + "px";
248 div.style.left=(x + 5) + "px";
248 div.style.left=(x + 5) + "px";
249
249
250 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
250 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
251 anim.animate();
251 anim.animate();
252 }
252 }
253
253
254 /**
254 /**
255 * This function will detect if selected period has some changesets for this user
255 * This function will detect if selected period has some changesets for this user
256 if it does this data is then pushed for displaying
256 if it does this data is then pushed for displaying
257 Additionally it will only display users that are selected by the checkbox
257 Additionally it will only display users that are selected by the checkbox
258 */
258 */
259 function getDataAccordingToRanges(ranges) {
259 function getDataAccordingToRanges(ranges) {
260
260
261 var data = [];
261 var data = [];
262 var keys = [];
262 var keys = [];
263 for(var key in dataset){
263 for(var key in dataset){
264 var push = false;
264 var push = false;
265 //method1 slow !!
265 //method1 slow !!
266 ///*
266 ///*
267 for(var ds in dataset[key].data){
267 for(var ds in dataset[key].data){
268 commit_data = dataset[key].data[ds];
268 commit_data = dataset[key].data[ds];
269 //console.log(key);
269 //console.log(key);
270 //console.log(new Date(commit_data.time*1000));
270 //console.log(new Date(commit_data.time*1000));
271 //console.log(new Date(ranges.xaxis.from*1000));
271 //console.log(new Date(ranges.xaxis.from*1000));
272 //console.log(new Date(ranges.xaxis.to*1000));
272 //console.log(new Date(ranges.xaxis.to*1000));
273 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
273 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
274 push = true;
274 push = true;
275 break;
275 break;
276 }
276 }
277 }
277 }
278 //*/
278 //*/
279 /*//method2 sorted commit data !!!
279 /*//method2 sorted commit data !!!
280 var first_commit = dataset[key].data[0].time;
280 var first_commit = dataset[key].data[0].time;
281 var last_commit = dataset[key].data[dataset[key].data.length-1].time;
281 var last_commit = dataset[key].data[dataset[key].data.length-1].time;
282
282
283 console.log(first_commit);
283 console.log(first_commit);
284 console.log(last_commit);
284 console.log(last_commit);
285
285
286 if (first_commit >= ranges.xaxis.from && last_commit <= ranges.xaxis.to){
286 if (first_commit >= ranges.xaxis.from && last_commit <= ranges.xaxis.to){
287 push = true;
287 push = true;
288 }
288 }
289 */
289 */
290 if(push){
290 if(push){
291 data.push(dataset[key]);
291 data.push(dataset[key]);
292 }
292 }
293 }
293 }
294 if(data.length >= 1){
294 if(data.length >= 1){
295 return data;
295 return data;
296 }
296 }
297 else{
297 else{
298 //just return dummy data for graph to plot itself
298 //just return dummy data for graph to plot itself
299 return [getDummyData('')];
299 return [getDummyData('')];
300 }
300 }
301
301
302 }
302 }
303
303
304 /**
304 /**
305 * redraw using new checkbox data
305 * redraw using new checkbox data
306 */
306 */
307 function plotchoiced(e,args){
307 function plotchoiced(e,args){
308 var cur_data = args[0];
308 var cur_data = args[0];
309 var cur_ranges = args[1];
309 var cur_ranges = args[1];
310
310
311 var new_data = [];
311 var new_data = [];
312 var inputs = choiceContainer.getElementsByTagName("input");
312 var inputs = choiceContainer.getElementsByTagName("input");
313
313
314 //show only checked labels
314 //show only checked labels
315 for(var i=0; i<inputs.length; i++) {
315 for(var i=0; i<inputs.length; i++) {
316 var checkbox_key = inputs[i].name;
316 var checkbox_key = inputs[i].name;
317
317
318 if(inputs[i].checked){
318 if(inputs[i].checked){
319 for(var d in cur_data){
319 for(var d in cur_data){
320 if(cur_data[d].label == checkbox_key){
320 if(cur_data[d].label == checkbox_key){
321 new_data.push(cur_data[d]);
321 new_data.push(cur_data[d]);
322 }
322 }
323 }
323 }
324 }
324 }
325 else{
325 else{
326 //push dummy data to not hide the label
326 //push dummy data to not hide the label
327 new_data.push(getDummyData(checkbox_key));
327 new_data.push(getDummyData(checkbox_key));
328 }
328 }
329 }
329 }
330
330
331 var new_options = YAHOO.lang.merge(plot_options, {
331 var new_options = YAHOO.lang.merge(plot_options, {
332 xaxis: {
332 xaxis: {
333 min: cur_ranges.xaxis.from,
333 min: cur_ranges.xaxis.from,
334 max: cur_ranges.xaxis.to,
334 max: cur_ranges.xaxis.to,
335 mode:"time",
335 mode:"time",
336 timeformat: "%d/%m",
336 timeformat: "%d/%m",
337 }
337 }
338 });
338 });
339 if (!new_data){
339 if (!new_data){
340 new_data = [[0,1]];
340 new_data = [[0,1]];
341 }
341 }
342 // do the zooming
342 // do the zooming
343 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
343 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
344
344
345 plot.subscribe("plotselected", plotselected);
345 plot.subscribe("plotselected", plotselected);
346
346
347 //resubscribe plothover
347 //resubscribe plothover
348 plot.subscribe("plothover", plothover);
348 plot.subscribe("plothover", plothover);
349
349
350 // don't fire event on the overview to prevent eternal loop
350 // don't fire event on the overview to prevent eternal loop
351 overview.setSelection(cur_ranges, true);
351 overview.setSelection(cur_ranges, true);
352
352
353 }
353 }
354
354
355 /**
355 /**
356 * plot only selected items from overview
356 * plot only selected items from overview
357 * @param ranges
357 * @param ranges
358 * @returns
358 * @returns
359 */
359 */
360 function plotselected(ranges,cur_data) {
360 function plotselected(ranges,cur_data) {
361 //updates the data for new plot
361 //updates the data for new plot
362 data = getDataAccordingToRanges(ranges);
362 data = getDataAccordingToRanges(ranges);
363 generateCheckboxes(data);
363 generateCheckboxes(data);
364
364
365 var new_options = YAHOO.lang.merge(plot_options, {
365 var new_options = YAHOO.lang.merge(plot_options, {
366 xaxis: {
366 xaxis: {
367 min: ranges.xaxis.from,
367 min: ranges.xaxis.from,
368 max: ranges.xaxis.to,
368 max: ranges.xaxis.to,
369 mode:"time",
369 mode:"time",
370 timeformat: "%d/%m",
370 timeformat: "%d/%m",
371 }
371 }
372 });
372 });
373 // do the zooming
373 // do the zooming
374 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
374 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
375
375
376 plot.subscribe("plotselected", plotselected);
376 plot.subscribe("plotselected", plotselected);
377
377
378 //resubscribe plothover
378 //resubscribe plothover
379 plot.subscribe("plothover", plothover);
379 plot.subscribe("plothover", plothover);
380
380
381 // don't fire event on the overview to prevent eternal loop
381 // don't fire event on the overview to prevent eternal loop
382 overview.setSelection(ranges, true);
382 overview.setSelection(ranges, true);
383
383
384 //resubscribe choiced
384 //resubscribe choiced
385 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
385 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
386 }
386 }
387
387
388 var previousPoint = null;
388 var previousPoint = null;
389
389
390 function plothover(o) {
390 function plothover(o) {
391 var pos = o.pos;
391 var pos = o.pos;
392 var item = o.item;
392 var item = o.item;
393
393
394 //YAHOO.util.Dom.get("x").innerHTML = pos.x.toFixed(2);
394 //YAHOO.util.Dom.get("x").innerHTML = pos.x.toFixed(2);
395 //YAHOO.util.Dom.get("y").innerHTML = pos.y.toFixed(2);
395 //YAHOO.util.Dom.get("y").innerHTML = pos.y.toFixed(2);
396 if (item) {
396 if (item) {
397 if (previousPoint != item.datapoint) {
397 if (previousPoint != item.datapoint) {
398 previousPoint = item.datapoint;
398 previousPoint = item.datapoint;
399
399
400 var tooltip = YAHOO.util.Dom.get("tooltip");
400 var tooltip = YAHOO.util.Dom.get("tooltip");
401 if(tooltip) {
401 if(tooltip) {
402 tooltip.parentNode.removeChild(tooltip);
402 tooltip.parentNode.removeChild(tooltip);
403 }
403 }
404 var x = item.datapoint.x.toFixed(2);
404 var x = item.datapoint.x.toFixed(2);
405 var y = item.datapoint.y.toFixed(2);
405 var y = item.datapoint.y.toFixed(2);
406
406
407 if (!item.series.label){
407 if (!item.series.label){
408 item.series.label = 'commits';
408 item.series.label = 'commits';
409 }
409 }
410 var d = new Date(x*1000);
410 var d = new Date(x*1000);
411 var fd = d.toDateString()
411 var fd = d.toDateString()
412 var nr_commits = parseInt(y);
412 var nr_commits = parseInt(y);
413
413
414 var cur_data = dataset[item.series.label].data[item.dataIndex];
414 var cur_data = dataset[item.series.label].data[item.dataIndex];
415 var added = cur_data.added;
415 var added = cur_data.added;
416 var changed = cur_data.changed;
416 var changed = cur_data.changed;
417 var removed = cur_data.removed;
417 var removed = cur_data.removed;
418
418
419 var nr_commits_suffix = " ${_('commits')} ";
419 var nr_commits_suffix = " ${_('commits')} ";
420 var added_suffix = " ${_('files added')} ";
420 var added_suffix = " ${_('files added')} ";
421 var changed_suffix = " ${_('files changed')} ";
421 var changed_suffix = " ${_('files changed')} ";
422 var removed_suffix = " ${_('files removed')} ";
422 var removed_suffix = " ${_('files removed')} ";
423
423
424
424
425 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
425 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
426 if(added==1){added_suffix=" ${_('file added')} ";}
426 if(added==1){added_suffix=" ${_('file added')} ";}
427 if(changed==1){changed_suffix=" ${_('file changed')} ";}
427 if(changed==1){changed_suffix=" ${_('file changed')} ";}
428 if(removed==1){removed_suffix=" ${_('file removed')} ";}
428 if(removed==1){removed_suffix=" ${_('file removed')} ";}
429
429
430 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
430 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
431 +'<br/>'+
431 +'<br/>'+
432 nr_commits + nr_commits_suffix+'<br/>'+
432 nr_commits + nr_commits_suffix+'<br/>'+
433 added + added_suffix +'<br/>'+
433 added + added_suffix +'<br/>'+
434 changed + changed_suffix + '<br/>'+
434 changed + changed_suffix + '<br/>'+
435 removed + removed_suffix + '<br/>');
435 removed + removed_suffix + '<br/>');
436 }
436 }
437 }
437 }
438 else {
438 else {
439 var tooltip = YAHOO.util.Dom.get("tooltip");
439 var tooltip = YAHOO.util.Dom.get("tooltip");
440
440
441 if(tooltip) {
441 if(tooltip) {
442 tooltip.parentNode.removeChild(tooltip);
442 tooltip.parentNode.removeChild(tooltip);
443 }
443 }
444 previousPoint = null;
444 previousPoint = null;
445 }
445 }
446 }
446 }
447
447
448 /**
448 /**
449 * MAIN EXECUTION
449 * MAIN EXECUTION
450 */
450 */
451
451
452 var data = getDataAccordingToRanges(initial_ranges);
452 var data = getDataAccordingToRanges(initial_ranges);
453 generateCheckboxes(data);
453 generateCheckboxes(data);
454
454
455 //main plot
455 //main plot
456 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
456 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
457
457
458 //overview
458 //overview
459 var overview = YAHOO.widget.Flot(overviewContainer, overview_dataset, overview_options);
459 var overview = YAHOO.widget.Flot(overviewContainer, overview_dataset, overview_options);
460
460
461 //show initial selection on overview
461 //show initial selection on overview
462 overview.setSelection(initial_ranges);
462 overview.setSelection(initial_ranges);
463
463
464 plot.subscribe("plotselected", plotselected);
464 plot.subscribe("plotselected", plotselected);
465
465
466 overview.subscribe("plotselected", function (ranges) {
466 overview.subscribe("plotselected", function (ranges) {
467 plot.setSelection(ranges);
467 plot.setSelection(ranges);
468 });
468 });
469
469
470 plot.subscribe("plothover", plothover);
470 plot.subscribe("plothover", plothover);
471
471
472 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
472 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
473 }
473 }
474 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
474 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
475 </script>
475 </script>
476
476
477 </div>
477 </div>
478 </div>
478 </div>
479
479
480 <div class="box">
480 <div class="box">
481 <div class="title">
481 <div class="title">
482 <div class="breadcrumbs">${h.link_to(_('Last ten changes'),h.url('changelog_home',repo_name=c.repo_name))}</div>
482 <div class="breadcrumbs">${h.link_to(_('Last ten changes'),h.url('changelog_home',repo_name=c.repo_name))}</div>
483 </div>
483 </div>
484 <div class="table">
484 <div class="table">
485 <%include file='../shortlog/shortlog_data.html'/>
485 <%include file='../shortlog/shortlog_data.html'/>
486 ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))}
486 ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))}
487 </div>
487 </div>
488 </div>
488 </div>
489 <div class="box">
489 <div class="box">
490 <div class="title">
490 <div class="title">
491 <div class="breadcrumbs">${h.link_to(_('Last ten tags'),h.url('tags_home',repo_name=c.repo_name))}</div>
491 <div class="breadcrumbs">${h.link_to(_('Last ten tags'),h.url('tags_home',repo_name=c.repo_name))}</div>
492 </div>
492 </div>
493 <div class="table">
493 <div class="table">
494 <%include file='../tags/tags_data.html'/>
494 <%include file='../tags/tags_data.html'/>
495 ${h.link_to(_('show more'),h.url('tags_home',repo_name=c.repo_name))}
495 ${h.link_to(_('show more'),h.url('tags_home',repo_name=c.repo_name))}
496 </div>
496 </div>
497 </div>
497 </div>
498 <div class="box">
498 <div class="box">
499 <div class="title">
499 <div class="title">
500 <div class="breadcrumbs">${h.link_to(_('Last ten branches'),h.url('branches_home',repo_name=c.repo_name))}</div>
500 <div class="breadcrumbs">${h.link_to(_('Last ten branches'),h.url('branches_home',repo_name=c.repo_name))}</div>
501 </div>
501 </div>
502 <div class="table">
502 <div class="table">
503 <%include file='../branches/branches_data.html'/>
503 <%include file='../branches/branches_data.html'/>
504 ${h.link_to(_('show more'),h.url('branches_home',repo_name=c.repo_name))}
504 ${h.link_to(_('show more'),h.url('branches_home',repo_name=c.repo_name))}
505 </div>
505 </div>
506 </div>
506 </div>
507
507
508 </%def> No newline at end of file
508 </%def>
General Comments 0
You need to be logged in to leave comments. Login now