Show More
@@ -0,0 +1,74 b'' | |||||
|
1 | # List of modules to import when celery starts. | |||
|
2 | import sys | |||
|
3 | import os | |||
|
4 | import ConfigParser | |||
|
5 | root = os.getcwd() | |||
|
6 | ||||
|
7 | PYLONS_CONFIG_NAME = 'development.ini' | |||
|
8 | ||||
|
9 | sys.path.append(root) | |||
|
10 | config = ConfigParser.ConfigParser({'here':root}) | |||
|
11 | config.read('%s/%s' % (root, PYLONS_CONFIG_NAME)) | |||
|
12 | PYLONS_CONFIG = config | |||
|
13 | ||||
|
14 | CELERY_IMPORTS = ("pylons_app.lib.celerylib.tasks",) | |||
|
15 | ||||
|
16 | ## Result store settings. | |||
|
17 | CELERY_RESULT_BACKEND = "database" | |||
|
18 | CELERY_RESULT_DBURI = dict(config.items('app:main'))['sqlalchemy.db1.url'] | |||
|
19 | CELERY_RESULT_SERIALIZER = 'json' | |||
|
20 | ||||
|
21 | ||||
|
22 | BROKER_CONNECTION_MAX_RETRIES = 30 | |||
|
23 | ||||
|
24 | ## Broker settings. | |||
|
25 | BROKER_HOST = "localhost" | |||
|
26 | BROKER_PORT = 5672 | |||
|
27 | BROKER_VHOST = "rabbitmqhost" | |||
|
28 | BROKER_USER = "rabbitmq" | |||
|
29 | BROKER_PASSWORD = "qweqwe" | |||
|
30 | ||||
|
31 | ## Worker settings | |||
|
32 | ## If you're doing mostly I/O you can have more processes, | |||
|
33 | ## but if mostly spending CPU, try to keep it close to the | |||
|
34 | ## number of CPUs on your machine. If not set, the number of CPUs/cores | |||
|
35 | ## available will be used. | |||
|
36 | CELERYD_CONCURRENCY = 2 | |||
|
37 | # CELERYD_LOG_FILE = "celeryd.log" | |||
|
38 | CELERYD_LOG_LEVEL = "DEBUG" | |||
|
39 | CELERYD_MAX_TASKS_PER_CHILD = 1 | |||
|
40 | ||||
|
41 | #Tasks will never be sent to the queue, but executed locally instead. | |||
|
42 | CELERY_ALWAYS_EAGER = False | |||
|
43 | ||||
|
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 ".*" ".*" ".*" |
@@ -0,0 +1,66 b'' | |||||
|
1 | from pylons_app.lib.pidlock import DaemonLock, LockHeld | |||
|
2 | from vcs.utils.lazy import LazyProperty | |||
|
3 | from decorator import decorator | |||
|
4 | import logging | |||
|
5 | import os | |||
|
6 | import sys | |||
|
7 | import traceback | |||
|
8 | from hashlib import md5 | |||
|
9 | log = logging.getLogger(__name__) | |||
|
10 | ||||
|
11 | class ResultWrapper(object): | |||
|
12 | def __init__(self, task): | |||
|
13 | self.task = task | |||
|
14 | ||||
|
15 | @LazyProperty | |||
|
16 | def result(self): | |||
|
17 | return self.task | |||
|
18 | ||||
|
19 | def run_task(task, *args, **kwargs): | |||
|
20 | try: | |||
|
21 | t = task.delay(*args, **kwargs) | |||
|
22 | log.info('running task %s', t.task_id) | |||
|
23 | return t | |||
|
24 | except Exception, e: | |||
|
25 | print e | |||
|
26 | if e.errno == 111: | |||
|
27 | log.debug('Unnable to connect. Sync execution') | |||
|
28 | else: | |||
|
29 | log.error(traceback.format_exc()) | |||
|
30 | #pure sync version | |||
|
31 | return ResultWrapper(task(*args, **kwargs)) | |||
|
32 | ||||
|
33 | ||||
|
34 | class LockTask(object): | |||
|
35 | """LockTask decorator""" | |||
|
36 | ||||
|
37 | def __init__(self, func): | |||
|
38 | self.func = func | |||
|
39 | ||||
|
40 | def __call__(self, func): | |||
|
41 | return decorator(self.__wrapper, func) | |||
|
42 | ||||
|
43 | def __wrapper(self, func, *fargs, **fkwargs): | |||
|
44 | params = [] | |||
|
45 | params.extend(fargs) | |||
|
46 | params.extend(fkwargs.values()) | |||
|
47 | lockkey = 'task_%s' % \ | |||
|
48 | md5(str(self.func) + '-' + '-'.join(map(str, params))).hexdigest() | |||
|
49 | log.info('running task with lockkey %s', lockkey) | |||
|
50 | try: | |||
|
51 | l = DaemonLock(lockkey) | |||
|
52 | return func(*fargs, **fkwargs) | |||
|
53 | l.release() | |||
|
54 | except LockHeld: | |||
|
55 | log.info('LockHeld') | |||
|
56 | return 'Task with key %s already running' % lockkey | |||
|
57 | ||||
|
58 | ||||
|
59 | ||||
|
60 | ||||
|
61 | ||||
|
62 | ||||
|
63 | ||||
|
64 | ||||
|
65 | ||||
|
66 |
@@ -0,0 +1,270 b'' | |||||
|
1 | from celery.decorators import task | |||
|
2 | from celery.task.sets import subtask | |||
|
3 | from celeryconfig import PYLONS_CONFIG as config | |||
|
4 | from pylons.i18n.translation import _ | |||
|
5 | from pylons_app.lib.celerylib import run_task, LockTask | |||
|
6 | from pylons_app.lib.helpers import person | |||
|
7 | from pylons_app.lib.smtp_mailer import SmtpMailer | |||
|
8 | from pylons_app.lib.utils import OrderedDict | |||
|
9 | from operator import itemgetter | |||
|
10 | from vcs.backends.hg import MercurialRepository | |||
|
11 | from time import mktime | |||
|
12 | import traceback | |||
|
13 | import json | |||
|
14 | ||||
|
15 | __all__ = ['whoosh_index', 'get_commits_stats', | |||
|
16 | 'reset_user_password', 'send_email'] | |||
|
17 | ||||
|
18 | def get_session(): | |||
|
19 | from sqlalchemy import engine_from_config | |||
|
20 | from sqlalchemy.orm import sessionmaker, scoped_session | |||
|
21 | engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.') | |||
|
22 | sa = scoped_session(sessionmaker(bind=engine)) | |||
|
23 | return sa | |||
|
24 | ||||
|
25 | def get_hg_settings(): | |||
|
26 | from pylons_app.model.db import HgAppSettings | |||
|
27 | try: | |||
|
28 | sa = get_session() | |||
|
29 | ret = sa.query(HgAppSettings).all() | |||
|
30 | finally: | |||
|
31 | sa.remove() | |||
|
32 | ||||
|
33 | if not ret: | |||
|
34 | raise Exception('Could not get application settings !') | |||
|
35 | settings = {} | |||
|
36 | for each in ret: | |||
|
37 | settings['hg_app_' + each.app_settings_name] = each.app_settings_value | |||
|
38 | ||||
|
39 | return settings | |||
|
40 | ||||
|
41 | def get_hg_ui_settings(): | |||
|
42 | from pylons_app.model.db import HgAppUi | |||
|
43 | try: | |||
|
44 | sa = get_session() | |||
|
45 | ret = sa.query(HgAppUi).all() | |||
|
46 | finally: | |||
|
47 | sa.remove() | |||
|
48 | ||||
|
49 | if not ret: | |||
|
50 | raise Exception('Could not get application ui settings !') | |||
|
51 | settings = {} | |||
|
52 | for each in ret: | |||
|
53 | k = each.ui_key | |||
|
54 | v = each.ui_value | |||
|
55 | if k == '/': | |||
|
56 | k = 'root_path' | |||
|
57 | ||||
|
58 | if k.find('.') != -1: | |||
|
59 | k = k.replace('.', '_') | |||
|
60 | ||||
|
61 | if each.ui_section == 'hooks': | |||
|
62 | v = each.ui_active | |||
|
63 | ||||
|
64 | settings[each.ui_section + '_' + k] = v | |||
|
65 | ||||
|
66 | return settings | |||
|
67 | ||||
|
68 | @task | |||
|
69 | def whoosh_index(repo_location, full_index): | |||
|
70 | log = whoosh_index.get_logger() | |||
|
71 | from pylons_app.lib.pidlock import DaemonLock | |||
|
72 | from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon, LockHeld | |||
|
73 | try: | |||
|
74 | l = DaemonLock() | |||
|
75 | WhooshIndexingDaemon(repo_location=repo_location)\ | |||
|
76 | .run(full_index=full_index) | |||
|
77 | l.release() | |||
|
78 | return 'Done' | |||
|
79 | except LockHeld: | |||
|
80 | log.info('LockHeld') | |||
|
81 | return 'LockHeld' | |||
|
82 | ||||
|
83 | ||||
|
84 | @task | |||
|
85 | @LockTask('get_commits_stats') | |||
|
86 | def get_commits_stats(repo_name, ts_min_y, ts_max_y): | |||
|
87 | author_key_cleaner = lambda k: person(k).replace('"', "") #for js data compatibilty | |||
|
88 | ||||
|
89 | from pylons_app.model.db import Statistics, Repository | |||
|
90 | log = get_commits_stats.get_logger() | |||
|
91 | commits_by_day_author_aggregate = {} | |||
|
92 | commits_by_day_aggregate = {} | |||
|
93 | repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '') | |||
|
94 | repo = MercurialRepository(repos_path + repo_name) | |||
|
95 | ||||
|
96 | skip_date_limit = True | |||
|
97 | parse_limit = 350 #limit for single task changeset parsing | |||
|
98 | last_rev = 0 | |||
|
99 | last_cs = None | |||
|
100 | timegetter = itemgetter('time') | |||
|
101 | ||||
|
102 | sa = get_session() | |||
|
103 | ||||
|
104 | dbrepo = sa.query(Repository)\ | |||
|
105 | .filter(Repository.repo_name == repo_name).scalar() | |||
|
106 | cur_stats = sa.query(Statistics)\ | |||
|
107 | .filter(Statistics.repository == dbrepo).scalar() | |||
|
108 | if cur_stats: | |||
|
109 | last_rev = cur_stats.stat_on_revision | |||
|
110 | ||||
|
111 | if last_rev == repo.revisions[-1]: | |||
|
112 | #pass silently without any work | |||
|
113 | return True | |||
|
114 | ||||
|
115 | if cur_stats: | |||
|
116 | commits_by_day_aggregate = OrderedDict( | |||
|
117 | json.loads( | |||
|
118 | cur_stats.commit_activity_combined)) | |||
|
119 | commits_by_day_author_aggregate = json.loads(cur_stats.commit_activity) | |||
|
120 | ||||
|
121 | for cnt, rev in enumerate(repo.revisions[last_rev:]): | |||
|
122 | last_cs = cs = repo.get_changeset(rev) | |||
|
123 | k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1], | |||
|
124 | cs.date.timetuple()[2]) | |||
|
125 | timetupple = [int(x) for x in k.split('-')] | |||
|
126 | timetupple.extend([0 for _ in xrange(6)]) | |||
|
127 | k = mktime(timetupple) | |||
|
128 | if commits_by_day_author_aggregate.has_key(author_key_cleaner(cs.author)): | |||
|
129 | try: | |||
|
130 | l = [timegetter(x) for x in commits_by_day_author_aggregate\ | |||
|
131 | [author_key_cleaner(cs.author)]['data']] | |||
|
132 | time_pos = l.index(k) | |||
|
133 | except ValueError: | |||
|
134 | time_pos = False | |||
|
135 | ||||
|
136 | if time_pos >= 0 and time_pos is not False: | |||
|
137 | ||||
|
138 | datadict = commits_by_day_author_aggregate\ | |||
|
139 | [author_key_cleaner(cs.author)]['data'][time_pos] | |||
|
140 | ||||
|
141 | datadict["commits"] += 1 | |||
|
142 | datadict["added"] += len(cs.added) | |||
|
143 | datadict["changed"] += len(cs.changed) | |||
|
144 | datadict["removed"] += len(cs.removed) | |||
|
145 | #print datadict | |||
|
146 | ||||
|
147 | else: | |||
|
148 | #print 'ELSE !!!!' | |||
|
149 | if k >= ts_min_y and k <= ts_max_y or skip_date_limit: | |||
|
150 | ||||
|
151 | datadict = {"time":k, | |||
|
152 | "commits":1, | |||
|
153 | "added":len(cs.added), | |||
|
154 | "changed":len(cs.changed), | |||
|
155 | "removed":len(cs.removed), | |||
|
156 | } | |||
|
157 | commits_by_day_author_aggregate\ | |||
|
158 | [author_key_cleaner(cs.author)]['data'].append(datadict) | |||
|
159 | ||||
|
160 | else: | |||
|
161 | #print k, 'nokey ADDING' | |||
|
162 | if k >= ts_min_y and k <= ts_max_y or skip_date_limit: | |||
|
163 | commits_by_day_author_aggregate[author_key_cleaner(cs.author)] = { | |||
|
164 | "label":author_key_cleaner(cs.author), | |||
|
165 | "data":[{"time":k, | |||
|
166 | "commits":1, | |||
|
167 | "added":len(cs.added), | |||
|
168 | "changed":len(cs.changed), | |||
|
169 | "removed":len(cs.removed), | |||
|
170 | }], | |||
|
171 | "schema":["commits"], | |||
|
172 | } | |||
|
173 | ||||
|
174 | # #gather all data by day | |||
|
175 | if commits_by_day_aggregate.has_key(k): | |||
|
176 | commits_by_day_aggregate[k] += 1 | |||
|
177 | else: | |||
|
178 | commits_by_day_aggregate[k] = 1 | |||
|
179 | ||||
|
180 | if cnt >= parse_limit: | |||
|
181 | #don't fetch to much data since we can freeze application | |||
|
182 | break | |||
|
183 | ||||
|
184 | overview_data = [] | |||
|
185 | for k, v in commits_by_day_aggregate.items(): | |||
|
186 | overview_data.append([k, v]) | |||
|
187 | overview_data = sorted(overview_data, key=itemgetter(0)) | |||
|
188 | ||||
|
189 | if not commits_by_day_author_aggregate: | |||
|
190 | commits_by_day_author_aggregate[author_key_cleaner(repo.contact)] = { | |||
|
191 | "label":author_key_cleaner(repo.contact), | |||
|
192 | "data":[0, 1], | |||
|
193 | "schema":["commits"], | |||
|
194 | } | |||
|
195 | ||||
|
196 | stats = cur_stats if cur_stats else Statistics() | |||
|
197 | stats.commit_activity = json.dumps(commits_by_day_author_aggregate) | |||
|
198 | stats.commit_activity_combined = json.dumps(overview_data) | |||
|
199 | stats.repository = dbrepo | |||
|
200 | stats.stat_on_revision = last_cs.revision | |||
|
201 | stats.languages = json.dumps({'_TOTAL_':0, '':0}) | |||
|
202 | ||||
|
203 | try: | |||
|
204 | sa.add(stats) | |||
|
205 | sa.commit() | |||
|
206 | except: | |||
|
207 | log.error(traceback.format_exc()) | |||
|
208 | sa.rollback() | |||
|
209 | return False | |||
|
210 | ||||
|
211 | run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y) | |||
|
212 | ||||
|
213 | return True | |||
|
214 | ||||
|
215 | @task | |||
|
216 | def reset_user_password(user_email): | |||
|
217 | log = reset_user_password.get_logger() | |||
|
218 | from pylons_app.lib import auth | |||
|
219 | from pylons_app.model.db import User | |||
|
220 | ||||
|
221 | try: | |||
|
222 | try: | |||
|
223 | sa = get_session() | |||
|
224 | user = sa.query(User).filter(User.email == user_email).scalar() | |||
|
225 | new_passwd = auth.PasswordGenerator().gen_password(8, | |||
|
226 | auth.PasswordGenerator.ALPHABETS_BIG_SMALL) | |||
|
227 | if user: | |||
|
228 | user.password = auth.get_crypt_password(new_passwd) | |||
|
229 | sa.add(user) | |||
|
230 | sa.commit() | |||
|
231 | log.info('change password for %s', user_email) | |||
|
232 | if new_passwd is None: | |||
|
233 | raise Exception('unable to generate new password') | |||
|
234 | ||||
|
235 | except: | |||
|
236 | log.error(traceback.format_exc()) | |||
|
237 | sa.rollback() | |||
|
238 | ||||
|
239 | run_task(send_email, user_email, | |||
|
240 | "Your new hg-app password", | |||
|
241 | 'Your new hg-app password:%s' % (new_passwd)) | |||
|
242 | log.info('send new password mail to %s', user_email) | |||
|
243 | ||||
|
244 | ||||
|
245 | except: | |||
|
246 | log.error('Failed to update user password') | |||
|
247 | log.error(traceback.format_exc()) | |||
|
248 | return True | |||
|
249 | ||||
|
250 | @task | |||
|
251 | def send_email(recipients, subject, body): | |||
|
252 | log = send_email.get_logger() | |||
|
253 | email_config = dict(config.items('DEFAULT')) | |||
|
254 | mail_from = email_config.get('app_email_from') | |||
|
255 | user = email_config.get('smtp_username') | |||
|
256 | passwd = email_config.get('smtp_password') | |||
|
257 | mail_server = email_config.get('smtp_server') | |||
|
258 | mail_port = email_config.get('smtp_port') | |||
|
259 | tls = email_config.get('smtp_use_tls') | |||
|
260 | ssl = False | |||
|
261 | ||||
|
262 | try: | |||
|
263 | m = SmtpMailer(mail_from, user, passwd, mail_server, | |||
|
264 | mail_port, ssl, tls) | |||
|
265 | m.send(recipients, subject, body) | |||
|
266 | except: | |||
|
267 | log.error('Mail sending failed') | |||
|
268 | log.error(traceback.format_exc()) | |||
|
269 | return False | |||
|
270 | return True |
@@ -0,0 +1,118 b'' | |||||
|
1 | import logging | |||
|
2 | import smtplib | |||
|
3 | import mimetypes | |||
|
4 | from email.mime.multipart import MIMEMultipart | |||
|
5 | from email.mime.image import MIMEImage | |||
|
6 | from email.mime.audio import MIMEAudio | |||
|
7 | from email.mime.base import MIMEBase | |||
|
8 | from email.mime.text import MIMEText | |||
|
9 | from email.utils import formatdate | |||
|
10 | from email import encoders | |||
|
11 | ||||
|
12 | class SmtpMailer(object): | |||
|
13 | """simple smtp mailer class | |||
|
14 | ||||
|
15 | mailer = SmtpMailer(mail_from, user, passwd, mail_server, mail_port, ssl, tls) | |||
|
16 | mailer.send(recipients, subject, body, attachment_files) | |||
|
17 | ||||
|
18 | :param recipients might be a list of string or single string | |||
|
19 | :param attachment_files is a dict of {filename:location} | |||
|
20 | it tries to guess the mimetype and attach the file | |||
|
21 | """ | |||
|
22 | ||||
|
23 | def __init__(self, mail_from, user, passwd, mail_server, | |||
|
24 | mail_port=None, ssl=False, tls=False): | |||
|
25 | ||||
|
26 | self.mail_from = mail_from | |||
|
27 | self.mail_server = mail_server | |||
|
28 | self.mail_port = mail_port | |||
|
29 | self.user = user | |||
|
30 | self.passwd = passwd | |||
|
31 | self.ssl = ssl | |||
|
32 | self.tls = tls | |||
|
33 | self.debug = False | |||
|
34 | ||||
|
35 | def send(self, recipients=[], subject='', body='', attachment_files={}): | |||
|
36 | ||||
|
37 | if isinstance(recipients, basestring): | |||
|
38 | recipients = [recipients] | |||
|
39 | if self.ssl: | |||
|
40 | smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port) | |||
|
41 | else: | |||
|
42 | smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port) | |||
|
43 | ||||
|
44 | if self.tls: | |||
|
45 | smtp_serv.starttls() | |||
|
46 | ||||
|
47 | if self.debug: | |||
|
48 | smtp_serv.set_debuglevel(1) | |||
|
49 | ||||
|
50 | smtp_serv.ehlo("mailer") | |||
|
51 | ||||
|
52 | #if server requires authorization you must provide login and password | |||
|
53 | smtp_serv.login(self.user, self.passwd) | |||
|
54 | ||||
|
55 | date_ = formatdate(localtime=True) | |||
|
56 | msg = MIMEMultipart() | |||
|
57 | msg['From'] = self.mail_from | |||
|
58 | msg['To'] = ','.join(recipients) | |||
|
59 | msg['Date'] = date_ | |||
|
60 | msg['Subject'] = subject | |||
|
61 | msg.preamble = 'You will not see this in a MIME-aware mail reader.\n' | |||
|
62 | ||||
|
63 | msg.attach(MIMEText(body)) | |||
|
64 | ||||
|
65 | if attachment_files: | |||
|
66 | self.__atach_files(msg, attachment_files) | |||
|
67 | ||||
|
68 | smtp_serv.sendmail(self.mail_from, recipients, msg.as_string()) | |||
|
69 | logging.info('MAIL SEND TO: %s' % recipients) | |||
|
70 | smtp_serv.quit() | |||
|
71 | ||||
|
72 | ||||
|
73 | def __atach_files(self, msg, attachment_files): | |||
|
74 | if isinstance(attachment_files, dict): | |||
|
75 | for f_name, msg_file in attachment_files.items(): | |||
|
76 | ctype, encoding = mimetypes.guess_type(f_name) | |||
|
77 | logging.info("guessing file %s type based on %s" , ctype, f_name) | |||
|
78 | if ctype is None or encoding is not None: | |||
|
79 | # No guess could be made, or the file is encoded (compressed), so | |||
|
80 | # use a generic bag-of-bits type. | |||
|
81 | ctype = 'application/octet-stream' | |||
|
82 | maintype, subtype = ctype.split('/', 1) | |||
|
83 | if maintype == 'text': | |||
|
84 | # Note: we should handle calculating the charset | |||
|
85 | file_part = MIMEText(self.get_content(msg_file), | |||
|
86 | _subtype=subtype) | |||
|
87 | elif maintype == 'image': | |||
|
88 | file_part = MIMEImage(self.get_content(msg_file), | |||
|
89 | _subtype=subtype) | |||
|
90 | elif maintype == 'audio': | |||
|
91 | file_part = MIMEAudio(self.get_content(msg_file), | |||
|
92 | _subtype=subtype) | |||
|
93 | else: | |||
|
94 | file_part = MIMEBase(maintype, subtype) | |||
|
95 | file_part.set_payload(self.get_content(msg_file)) | |||
|
96 | # Encode the payload using Base64 | |||
|
97 | encoders.encode_base64(msg) | |||
|
98 | # Set the filename parameter | |||
|
99 | file_part.add_header('Content-Disposition', 'attachment', | |||
|
100 | filename=f_name) | |||
|
101 | file_part.add_header('Content-Type', ctype, name=f_name) | |||
|
102 | msg.attach(file_part) | |||
|
103 | else: | |||
|
104 | raise Exception('Attachment files should be' | |||
|
105 | 'a dict in format {"filename":"filepath"}') | |||
|
106 | ||||
|
107 | def get_content(self, msg_file): | |||
|
108 | ''' | |||
|
109 | Get content based on type, if content is a string do open first | |||
|
110 | else just read because it's a probably open file object | |||
|
111 | @param msg_file: | |||
|
112 | ''' | |||
|
113 | if isinstance(msg_file, str): | |||
|
114 | return open(msg_file, "rb").read() | |||
|
115 | else: | |||
|
116 | #just for safe seek to 0 | |||
|
117 | msg_file.seek(0) | |||
|
118 | return msg_file.read() |
@@ -0,0 +1,267 b'' | |||||
|
1 | """caching_query.py | |||
|
2 | ||||
|
3 | Represent persistence structures which allow the usage of | |||
|
4 | Beaker caching with SQLAlchemy. | |||
|
5 | ||||
|
6 | The three new concepts introduced here are: | |||
|
7 | ||||
|
8 | * CachingQuery - a Query subclass that caches and | |||
|
9 | retrieves results in/from Beaker. | |||
|
10 | * FromCache - a query option that establishes caching | |||
|
11 | parameters on a Query | |||
|
12 | * RelationshipCache - a variant of FromCache which is specific | |||
|
13 | to a query invoked during a lazy load. | |||
|
14 | * _params_from_query - extracts value parameters from | |||
|
15 | a Query. | |||
|
16 | ||||
|
17 | The rest of what's here are standard SQLAlchemy and | |||
|
18 | Beaker constructs. | |||
|
19 | ||||
|
20 | """ | |||
|
21 | from sqlalchemy.orm.interfaces import MapperOption | |||
|
22 | from sqlalchemy.orm.query import Query | |||
|
23 | from sqlalchemy.sql import visitors | |||
|
24 | ||||
|
25 | class CachingQuery(Query): | |||
|
26 | """A Query subclass which optionally loads full results from a Beaker | |||
|
27 | cache region. | |||
|
28 | ||||
|
29 | The CachingQuery stores additional state that allows it to consult | |||
|
30 | a Beaker cache before accessing the database: | |||
|
31 | ||||
|
32 | * A "region", which is a cache region argument passed to a | |||
|
33 | Beaker CacheManager, specifies a particular cache configuration | |||
|
34 | (including backend implementation, expiration times, etc.) | |||
|
35 | * A "namespace", which is a qualifying name that identifies a | |||
|
36 | group of keys within the cache. A query that filters on a name | |||
|
37 | might use the name "by_name", a query that filters on a date range | |||
|
38 | to a joined table might use the name "related_date_range". | |||
|
39 | ||||
|
40 | When the above state is present, a Beaker cache is retrieved. | |||
|
41 | ||||
|
42 | The "namespace" name is first concatenated with | |||
|
43 | a string composed of the individual entities and columns the Query | |||
|
44 | requests, i.e. such as ``Query(User.id, User.name)``. | |||
|
45 | ||||
|
46 | The Beaker cache is then loaded from the cache manager based | |||
|
47 | on the region and composed namespace. The key within the cache | |||
|
48 | itself is then constructed against the bind parameters specified | |||
|
49 | by this query, which are usually literals defined in the | |||
|
50 | WHERE clause. | |||
|
51 | ||||
|
52 | The FromCache and RelationshipCache mapper options below represent | |||
|
53 | the "public" method of configuring this state upon the CachingQuery. | |||
|
54 | ||||
|
55 | """ | |||
|
56 | ||||
|
57 | def __init__(self, manager, *args, **kw): | |||
|
58 | self.cache_manager = manager | |||
|
59 | Query.__init__(self, *args, **kw) | |||
|
60 | ||||
|
61 | def __iter__(self): | |||
|
62 | """override __iter__ to pull results from Beaker | |||
|
63 | if particular attributes have been configured. | |||
|
64 | ||||
|
65 | Note that this approach does *not* detach the loaded objects from | |||
|
66 | the current session. If the cache backend is an in-process cache | |||
|
67 | (like "memory") and lives beyond the scope of the current session's | |||
|
68 | transaction, those objects may be expired. The method here can be | |||
|
69 | modified to first expunge() each loaded item from the current | |||
|
70 | session before returning the list of items, so that the items | |||
|
71 | in the cache are not the same ones in the current Session. | |||
|
72 | ||||
|
73 | """ | |||
|
74 | if hasattr(self, '_cache_parameters'): | |||
|
75 | return self.get_value(createfunc=lambda: list(Query.__iter__(self))) | |||
|
76 | else: | |||
|
77 | return Query.__iter__(self) | |||
|
78 | ||||
|
79 | def invalidate(self): | |||
|
80 | """Invalidate the value represented by this Query.""" | |||
|
81 | ||||
|
82 | cache, cache_key = _get_cache_parameters(self) | |||
|
83 | cache.remove(cache_key) | |||
|
84 | ||||
|
85 | def get_value(self, merge=True, createfunc=None): | |||
|
86 | """Return the value from the cache for this query. | |||
|
87 | ||||
|
88 | Raise KeyError if no value present and no | |||
|
89 | createfunc specified. | |||
|
90 | ||||
|
91 | """ | |||
|
92 | cache, cache_key = _get_cache_parameters(self) | |||
|
93 | ret = cache.get_value(cache_key, createfunc=createfunc) | |||
|
94 | if merge: | |||
|
95 | ret = self.merge_result(ret, load=False) | |||
|
96 | return ret | |||
|
97 | ||||
|
98 | def set_value(self, value): | |||
|
99 | """Set the value in the cache for this query.""" | |||
|
100 | ||||
|
101 | cache, cache_key = _get_cache_parameters(self) | |||
|
102 | cache.put(cache_key, value) | |||
|
103 | ||||
|
104 | def query_callable(manager): | |||
|
105 | def query(*arg, **kw): | |||
|
106 | return CachingQuery(manager, *arg, **kw) | |||
|
107 | return query | |||
|
108 | ||||
|
109 | def _get_cache_parameters(query): | |||
|
110 | """For a query with cache_region and cache_namespace configured, | |||
|
111 | return the correspoinding Cache instance and cache key, based | |||
|
112 | on this query's current criterion and parameter values. | |||
|
113 | ||||
|
114 | """ | |||
|
115 | if not hasattr(query, '_cache_parameters'): | |||
|
116 | raise ValueError("This Query does not have caching parameters configured.") | |||
|
117 | ||||
|
118 | region, namespace, cache_key = query._cache_parameters | |||
|
119 | ||||
|
120 | namespace = _namespace_from_query(namespace, query) | |||
|
121 | ||||
|
122 | if cache_key is None: | |||
|
123 | # cache key - the value arguments from this query's parameters. | |||
|
124 | args = _params_from_query(query) | |||
|
125 | cache_key = " ".join([str(x) for x in args]) | |||
|
126 | ||||
|
127 | # get cache | |||
|
128 | cache = query.cache_manager.get_cache_region(namespace, region) | |||
|
129 | ||||
|
130 | # optional - hash the cache_key too for consistent length | |||
|
131 | # import uuid | |||
|
132 | # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key)) | |||
|
133 | ||||
|
134 | return cache, cache_key | |||
|
135 | ||||
|
136 | def _namespace_from_query(namespace, query): | |||
|
137 | # cache namespace - the token handed in by the | |||
|
138 | # option + class we're querying against | |||
|
139 | namespace = " ".join([namespace] + [str(x) for x in query._entities]) | |||
|
140 | ||||
|
141 | # memcached wants this | |||
|
142 | namespace = namespace.replace(' ', '_') | |||
|
143 | ||||
|
144 | return namespace | |||
|
145 | ||||
|
146 | def _set_cache_parameters(query, region, namespace, cache_key): | |||
|
147 | ||||
|
148 | if hasattr(query, '_cache_parameters'): | |||
|
149 | region, namespace, cache_key = query._cache_parameters | |||
|
150 | raise ValueError("This query is already configured " | |||
|
151 | "for region %r namespace %r" % | |||
|
152 | (region, namespace) | |||
|
153 | ) | |||
|
154 | query._cache_parameters = region, namespace, cache_key | |||
|
155 | ||||
|
156 | class FromCache(MapperOption): | |||
|
157 | """Specifies that a Query should load results from a cache.""" | |||
|
158 | ||||
|
159 | propagate_to_loaders = False | |||
|
160 | ||||
|
161 | def __init__(self, region, namespace, cache_key=None): | |||
|
162 | """Construct a new FromCache. | |||
|
163 | ||||
|
164 | :param region: the cache region. Should be a | |||
|
165 | region configured in the Beaker CacheManager. | |||
|
166 | ||||
|
167 | :param namespace: the cache namespace. Should | |||
|
168 | be a name uniquely describing the target Query's | |||
|
169 | lexical structure. | |||
|
170 | ||||
|
171 | :param cache_key: optional. A string cache key | |||
|
172 | that will serve as the key to the query. Use this | |||
|
173 | if your query has a huge amount of parameters (such | |||
|
174 | as when using in_()) which correspond more simply to | |||
|
175 | some other identifier. | |||
|
176 | ||||
|
177 | """ | |||
|
178 | self.region = region | |||
|
179 | self.namespace = namespace | |||
|
180 | self.cache_key = cache_key | |||
|
181 | ||||
|
182 | def process_query(self, query): | |||
|
183 | """Process a Query during normal loading operation.""" | |||
|
184 | ||||
|
185 | _set_cache_parameters(query, self.region, self.namespace, self.cache_key) | |||
|
186 | ||||
|
187 | class RelationshipCache(MapperOption): | |||
|
188 | """Specifies that a Query as called within a "lazy load" | |||
|
189 | should load results from a cache.""" | |||
|
190 | ||||
|
191 | propagate_to_loaders = True | |||
|
192 | ||||
|
193 | def __init__(self, region, namespace, attribute): | |||
|
194 | """Construct a new RelationshipCache. | |||
|
195 | ||||
|
196 | :param region: the cache region. Should be a | |||
|
197 | region configured in the Beaker CacheManager. | |||
|
198 | ||||
|
199 | :param namespace: the cache namespace. Should | |||
|
200 | be a name uniquely describing the target Query's | |||
|
201 | lexical structure. | |||
|
202 | ||||
|
203 | :param attribute: A Class.attribute which | |||
|
204 | indicates a particular class relationship() whose | |||
|
205 | lazy loader should be pulled from the cache. | |||
|
206 | ||||
|
207 | """ | |||
|
208 | self.region = region | |||
|
209 | self.namespace = namespace | |||
|
210 | self._relationship_options = { | |||
|
211 | (attribute.property.parent.class_, attribute.property.key) : self | |||
|
212 | } | |||
|
213 | ||||
|
214 | def process_query_conditionally(self, query): | |||
|
215 | """Process a Query that is used within a lazy loader. | |||
|
216 | ||||
|
217 | (the process_query_conditionally() method is a SQLAlchemy | |||
|
218 | hook invoked only within lazyload.) | |||
|
219 | ||||
|
220 | """ | |||
|
221 | if query._current_path: | |||
|
222 | mapper, key = query._current_path[-2:] | |||
|
223 | ||||
|
224 | for cls in mapper.class_.__mro__: | |||
|
225 | if (cls, key) in self._relationship_options: | |||
|
226 | relationship_option = self._relationship_options[(cls, key)] | |||
|
227 | _set_cache_parameters( | |||
|
228 | query, | |||
|
229 | relationship_option.region, | |||
|
230 | relationship_option.namespace, | |||
|
231 | None) | |||
|
232 | ||||
|
233 | def and_(self, option): | |||
|
234 | """Chain another RelationshipCache option to this one. | |||
|
235 | ||||
|
236 | While many RelationshipCache objects can be specified on a single | |||
|
237 | Query separately, chaining them together allows for a more efficient | |||
|
238 | lookup during load. | |||
|
239 | ||||
|
240 | """ | |||
|
241 | self._relationship_options.update(option._relationship_options) | |||
|
242 | return self | |||
|
243 | ||||
|
244 | ||||
|
245 | def _params_from_query(query): | |||
|
246 | """Pull the bind parameter values from a query. | |||
|
247 | ||||
|
248 | This takes into account any scalar attribute bindparam set up. | |||
|
249 | ||||
|
250 | E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7))) | |||
|
251 | would return [5, 7]. | |||
|
252 | ||||
|
253 | """ | |||
|
254 | v = [] | |||
|
255 | def visit_bindparam(bind): | |||
|
256 | value = query._params.get(bind.key, bind.value) | |||
|
257 | ||||
|
258 | # lazyloader may dig a callable in here, intended | |||
|
259 | # to late-evaluate params after autoflush is called. | |||
|
260 | # convert to a scalar value. | |||
|
261 | if callable(value): | |||
|
262 | value = value() | |||
|
263 | ||||
|
264 | v.append(value) | |||
|
265 | if query._criterion is not None: | |||
|
266 | visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam}) | |||
|
267 | return v |
@@ -0,0 +1,54 b'' | |||||
|
1 | ## -*- coding: utf-8 -*- | |||
|
2 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |||
|
3 | <html xmlns="http://www.w3.org/1999/xhtml" id="mainhtml"> | |||
|
4 | <head> | |||
|
5 | <title>${_('Reset You password to hg-app')}</title> | |||
|
6 | <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> | |||
|
7 | <link rel="icon" href="/images/hgicon.png" type="image/png" /> | |||
|
8 | <meta name="robots" content="index, nofollow"/> | |||
|
9 | ||||
|
10 | <!-- stylesheets --> | |||
|
11 | <link rel="stylesheet" type="text/css" href="/css/reset.css" /> | |||
|
12 | <link rel="stylesheet" type="text/css" href="/css/style.css" media="screen" /> | |||
|
13 | <link id="color" rel="stylesheet" type="text/css" href="/css/colors/blue.css" /> | |||
|
14 | ||||
|
15 | <!-- scripts --> | |||
|
16 | ||||
|
17 | </head> | |||
|
18 | <body> | |||
|
19 | <div id="register"> | |||
|
20 | ||||
|
21 | <div class="title"> | |||
|
22 | <h5>${_('Reset You password to hg-app')}</h5> | |||
|
23 | <div class="corner tl"></div> | |||
|
24 | <div class="corner tr"></div> | |||
|
25 | </div> | |||
|
26 | <div class="inner"> | |||
|
27 | ${h.form(url('password_reset'))} | |||
|
28 | <div class="form"> | |||
|
29 | <!-- fields --> | |||
|
30 | <div class="fields"> | |||
|
31 | ||||
|
32 | <div class="field"> | |||
|
33 | <div class="label"> | |||
|
34 | <label for="email">${_('Email address')}:</label> | |||
|
35 | </div> | |||
|
36 | <div class="input"> | |||
|
37 | ${h.text('email')} | |||
|
38 | </div> | |||
|
39 | </div> | |||
|
40 | ||||
|
41 | <div class="buttons"> | |||
|
42 | <div class="nohighlight"> | |||
|
43 | ${h.submit('send','Reset my password',class_="ui-button ui-widget ui-state-default ui-corner-all")} | |||
|
44 | <div class="activation_msg">${_('Your new password will be send to matching email address')}</div> | |||
|
45 | </div> | |||
|
46 | </div> | |||
|
47 | </div> | |||
|
48 | </div> | |||
|
49 | ${h.end_form()} | |||
|
50 | </div> | |||
|
51 | </div> | |||
|
52 | </body> | |||
|
53 | </html> | |||
|
54 |
@@ -11,9 +11,12 b' Fully customizable, with authentication,' | |||||
11 | - full permissions per project read/write/admin access even on mercurial request |
|
11 | - full permissions per project read/write/admin access even on mercurial request | |
12 | - mako templates let's you cusmotize look and feel of application. |
|
12 | - mako templates let's you cusmotize look and feel of application. | |
13 | - diffs annotations and source code all colored by pygments. |
|
13 | - diffs annotations and source code all colored by pygments. | |
14 | - mercurial branch graph and yui-flot powered graphs |
|
14 | - mercurial branch graph and yui-flot powered graphs with zooming | |
15 | - admin interface for performing user/permission managments as well as repository |
|
15 | - admin interface for performing user/permission managments as well as repository | |
16 | managment. |
|
16 | managment. | |
|
17 | - full text search of source codes with indexing daemons using whoosh | |||
|
18 | (no external search servers required all in one application) | |||
|
19 | - async tasks for speed and performance using celery (works without them too) | |||
17 | - Additional settings for mercurial web, (hooks editable from admin |
|
20 | - Additional settings for mercurial web, (hooks editable from admin | |
18 | panel !) also manage paths, archive, remote messages |
|
21 | panel !) also manage paths, archive, remote messages | |
19 | - backup scripts can do backup of whole app and send it over scp to desired location |
|
22 | - backup scripts can do backup of whole app and send it over scp to desired location | |
@@ -27,11 +30,11 b' Fully customizable, with authentication,' | |||||
27 | **Incoming** |
|
30 | **Incoming** | |
28 |
|
31 | |||
29 | - code review based on hg-review (when it's stable) |
|
32 | - code review based on hg-review (when it's stable) | |
30 | - git support (when vcs can handle it) |
|
33 | - git support (when vcs can handle it - almost there !) | |
31 | - full text search of source codes with indexing daemons using whoosh |
|
34 | - commit based wikis | |
32 | (no external search servers required all in one application) |
|
35 | - in server forks | |
33 | - manage hg ui() per repo, add hooks settings, per repo, and not globally |
|
36 | - clonning from remote repositories into hg-app | |
34 | - other cools stuff that i can figure out |
|
37 | - other cools stuff that i can figure out (or You can help me figure out) | |
35 |
|
38 | |||
36 | .. note:: |
|
39 | .. note:: | |
37 | This software is still in beta mode. |
|
40 | This software is still in beta mode. | |
@@ -47,10 +50,10 b' Installation' | |||||
47 |
|
50 | |||
48 | - create new virtualenv and activate it - highly recommend that you use separate |
|
51 | - create new virtualenv and activate it - highly recommend that you use separate | |
49 | virtual-env for whole application |
|
52 | virtual-env for whole application | |
50 |
- download hg app from default |
|
53 | - download hg app from default branch from bitbucket and run | |
51 | 'python setup.py install' this will install all required dependencies needed |
|
54 | 'python setup.py install' this will install all required dependencies needed | |
52 | - run paster setup-app production.ini it should create all needed tables |
|
55 | - run paster setup-app production.ini it should create all needed tables | |
53 | and an admin account. |
|
56 | and an admin account make sure You specify correct path to repositories. | |
54 | - remember that the given path for mercurial repositories must be write |
|
57 | - remember that the given path for mercurial repositories must be write | |
55 | accessible for the application |
|
58 | accessible for the application | |
56 | - run paster serve development.ini - or you can use manage-hg_app script. |
|
59 | - run paster serve development.ini - or you can use manage-hg_app script. | |
@@ -58,4 +61,9 b' Installation' | |||||
58 | - use admin account you created to login. |
|
61 | - use admin account you created to login. | |
59 | - default permissions on each repository is read, and owner is admin. So remember |
|
62 | - default permissions on each repository is read, and owner is admin. So remember | |
60 | to update these. |
|
63 | to update these. | |
|
64 | - in order to use full power of async tasks, You must install message broker | |||
|
65 | preferrably rabbitmq and start celeryd daemon. The app should gain some speed | |||
|
66 | than. For installation instructions | |||
|
67 | You can visit: http://ask.github.com/celery/getting-started/index.html. All | |||
|
68 | needed configs are inside hg-app ie. celeryconfig.py | |||
61 | No newline at end of file |
|
69 |
@@ -1,32 +1,37 b'' | |||||
1 | ################################################################################ |
|
1 | ################################################################################ | |
2 | ################################################################################ |
|
2 | ################################################################################ | |
3 |
# |
|
3 | # hg-app - Pylons environment configuration # | |
4 | # # |
|
4 | # # | |
5 | # The %(here)s variable will be replaced with the parent directory of this file# |
|
5 | # The %(here)s variable will be replaced with the parent directory of this file# | |
6 | ################################################################################ |
|
6 | ################################################################################ | |
7 |
|
7 | |||
8 | [DEFAULT] |
|
8 | [DEFAULT] | |
9 | debug = true |
|
9 | debug = true | |
10 | ############################################ |
|
10 | ################################################################################ | |
11 | ## Uncomment and replace with the address ## |
|
11 | ## Uncomment and replace with the address which should receive ## | |
12 | ## which should receive any error reports ## |
|
12 | ## any error reports after application crash ## | |
13 | ############################################ |
|
13 | ## Additionally those settings will be used by hg-app mailing system ## | |
|
14 | ################################################################################ | |||
14 | #email_to = admin@localhost |
|
15 | #email_to = admin@localhost | |
|
16 | #error_email_from = paste_error@localhost | |||
|
17 | #app_email_from = hg-app-noreply@localhost | |||
|
18 | #error_message = | |||
|
19 | ||||
15 | #smtp_server = mail.server.com |
|
20 | #smtp_server = mail.server.com | |
16 | #error_email_from = paste_error@localhost |
|
|||
17 | #smtp_username = |
|
21 | #smtp_username = | |
18 |
#smtp_password = |
|
22 | #smtp_password = | |
19 | #error_message = 'mercurial crash !' |
|
23 | #smtp_port = | |
|
24 | #smtp_use_tls = | |||
20 |
|
25 | |||
21 | [server:main] |
|
26 | [server:main] | |
22 | ##nr of threads to spawn |
|
27 | ##nr of threads to spawn | |
23 | threadpool_workers = 5 |
|
28 | threadpool_workers = 5 | |
24 |
|
29 | |||
25 | ##max request before |
|
30 | ##max request before | |
26 |
threadpool_max_requests = |
|
31 | threadpool_max_requests = 6 | |
27 |
|
32 | |||
28 | ##option to use threads of process |
|
33 | ##option to use threads of process | |
29 |
use_threadpool = |
|
34 | use_threadpool = false | |
30 |
|
35 | |||
31 | use = egg:Paste#http |
|
36 | use = egg:Paste#http | |
32 | host = 127.0.0.1 |
|
37 | host = 127.0.0.1 | |
@@ -56,7 +61,7 b' beaker.cache.super_short_term.expire=10' | |||||
56 | ### BEAKER SESSION #### |
|
61 | ### BEAKER SESSION #### | |
57 | #################################### |
|
62 | #################################### | |
58 | ## Type of storage used for the session, current types are |
|
63 | ## Type of storage used for the session, current types are | |
59 |
## |
|
64 | ## "dbm", "file", "memcached", "database", and "memory". | |
60 | ## The storage uses the Container API |
|
65 | ## The storage uses the Container API | |
61 | ##that is also used by the cache system. |
|
66 | ##that is also used by the cache system. | |
62 | beaker.session.type = file |
|
67 | beaker.session.type = file |
@@ -1,28 +1,33 b'' | |||||
1 | ################################################################################ |
|
1 | ################################################################################ | |
2 | ################################################################################ |
|
2 | ################################################################################ | |
3 |
# |
|
3 | # hg-app - Pylons environment configuration # | |
4 | # # |
|
4 | # # | |
5 | # The %(here)s variable will be replaced with the parent directory of this file# |
|
5 | # The %(here)s variable will be replaced with the parent directory of this file# | |
6 | ################################################################################ |
|
6 | ################################################################################ | |
7 |
|
7 | |||
8 | [DEFAULT] |
|
8 | [DEFAULT] | |
9 | debug = true |
|
9 | debug = true | |
10 | ############################################ |
|
10 | ################################################################################ | |
11 | ## Uncomment and replace with the address ## |
|
11 | ## Uncomment and replace with the address which should receive ## | |
12 | ## which should receive any error reports ## |
|
12 | ## any error reports after application crash ## | |
13 | ############################################ |
|
13 | ## Additionally those settings will be used by hg-app mailing system ## | |
|
14 | ################################################################################ | |||
14 | #email_to = admin@localhost |
|
15 | #email_to = admin@localhost | |
|
16 | #error_email_from = paste_error@localhost | |||
|
17 | #app_email_from = hg-app-noreply@localhost | |||
|
18 | #error_message = | |||
|
19 | ||||
15 | #smtp_server = mail.server.com |
|
20 | #smtp_server = mail.server.com | |
16 | #error_email_from = paste_error@localhost |
|
|||
17 | #smtp_username = |
|
21 | #smtp_username = | |
18 | #smtp_password = |
|
22 | #smtp_password = | |
19 | #error_message = 'mercurial crash !' |
|
23 | #smtp_port = | |
|
24 | #smtp_use_tls = false | |||
20 |
|
25 | |||
21 | [server:main] |
|
26 | [server:main] | |
22 | ##nr of threads to spawn |
|
27 | ##nr of threads to spawn | |
23 | threadpool_workers = 5 |
|
28 | threadpool_workers = 5 | |
24 |
|
29 | |||
25 | ##max request before |
|
30 | ##max request before thread respawn | |
26 | threadpool_max_requests = 2 |
|
31 | threadpool_max_requests = 2 | |
27 |
|
32 | |||
28 | ##option to use threads of process |
|
33 | ##option to use threads of process |
@@ -20,10 +20,11 b'' | |||||
20 | """ |
|
20 | """ | |
21 | Created on April 9, 2010 |
|
21 | Created on April 9, 2010 | |
22 | Hg app, a web based mercurial repository managment based on pylons |
|
22 | Hg app, a web based mercurial repository managment based on pylons | |
|
23 | versioning implementation: http://semver.org/ | |||
23 | @author: marcink |
|
24 | @author: marcink | |
24 | """ |
|
25 | """ | |
25 |
|
26 | |||
26 |
VERSION = (0, 8, |
|
27 | VERSION = (0, 8, 3, 'beta') | |
27 |
|
28 | |||
28 | __version__ = '.'.join((str(each) for each in VERSION[:4])) |
|
29 | __version__ = '.'.join((str(each) for each in VERSION[:4])) | |
29 |
|
30 |
@@ -7,16 +7,21 b'' | |||||
7 |
|
7 | |||
8 | [DEFAULT] |
|
8 | [DEFAULT] | |
9 | debug = true |
|
9 | debug = true | |
10 | ############################################ |
|
10 | ################################################################################ | |
11 | ## Uncomment and replace with the address ## |
|
11 | ## Uncomment and replace with the address which should receive ## | |
12 | ## which should receive any error reports ## |
|
12 | ## any error reports after application crash ## | |
13 | ############################################ |
|
13 | ## Additionally those settings will be used by hg-app mailing system ## | |
|
14 | ################################################################################ | |||
14 | #email_to = admin@localhost |
|
15 | #email_to = admin@localhost | |
|
16 | #error_email_from = paste_error@localhost | |||
|
17 | #app_email_from = hg-app-noreply@localhost | |||
|
18 | #error_message = | |||
|
19 | ||||
15 | #smtp_server = mail.server.com |
|
20 | #smtp_server = mail.server.com | |
16 | #error_email_from = paste_error@localhost |
|
|||
17 | #smtp_username = |
|
21 | #smtp_username = | |
18 | #smtp_password = |
|
22 | #smtp_password = | |
19 | #error_message = 'hp-app crash !' |
|
23 | #smtp_port = | |
|
24 | #smtp_use_tls = false | |||
20 |
|
25 | |||
21 | [server:main] |
|
26 | [server:main] | |
22 | ##nr of threads to spawn |
|
27 | ##nr of threads to spawn |
@@ -49,7 +49,12 b' def load_environment(global_conf, app_co' | |||||
49 |
|
49 | |||
50 | #sets the c attribute access when don't existing attribute are accessed |
|
50 | #sets the c attribute access when don't existing attribute are accessed | |
51 | config['pylons.strict_tmpl_context'] = True |
|
51 | config['pylons.strict_tmpl_context'] = True | |
52 |
test = os.path.split(config['__file__'])[-1] == 'test |
|
52 | test = os.path.split(config['__file__'])[-1] == 'test.ini' | |
|
53 | if test: | |||
|
54 | from pylons_app.lib.utils import create_test_env, create_test_index | |||
|
55 | create_test_env('/tmp', config) | |||
|
56 | create_test_index('/tmp/*', True) | |||
|
57 | ||||
53 | #MULTIPLE DB configs |
|
58 | #MULTIPLE DB configs | |
54 | # Setup the SQLAlchemy database engine |
|
59 | # Setup the SQLAlchemy database engine | |
55 | if config['debug'] and not test: |
|
60 | if config['debug'] and not test: |
@@ -110,10 +110,11 b' def make_map(config):' | |||||
110 | #SEARCH |
|
110 | #SEARCH | |
111 | map.connect('search', '/_admin/search', controller='search') |
|
111 | map.connect('search', '/_admin/search', controller='search') | |
112 |
|
112 | |||
113 | #LOGIN/LOGOUT |
|
113 | #LOGIN/LOGOUT/REGISTER/SIGN IN | |
114 | map.connect('login_home', '/_admin/login', controller='login') |
|
114 | map.connect('login_home', '/_admin/login', controller='login') | |
115 | map.connect('logout_home', '/_admin/logout', controller='login', action='logout') |
|
115 | map.connect('logout_home', '/_admin/logout', controller='login', action='logout') | |
116 | map.connect('register', '/_admin/register', controller='login', action='register') |
|
116 | map.connect('register', '/_admin/register', controller='login', action='register') | |
|
117 | map.connect('reset_password', '/_admin/password_reset', controller='login', action='password_reset') | |||
117 |
|
118 | |||
118 | #FEEDS |
|
119 | #FEEDS | |
119 | map.connect('rss_feed_home', '/{repo_name:.*}/feed/rss', |
|
120 | map.connect('rss_feed_home', '/{repo_name:.*}/feed/rss', | |
@@ -147,9 +148,12 b' def make_map(config):' | |||||
147 | map.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}', |
|
148 | map.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}', | |
148 | controller='files', action='diff', revision='tip', f_path='', |
|
149 | controller='files', action='diff', revision='tip', f_path='', | |
149 | conditions=dict(function=check_repo)) |
|
150 | conditions=dict(function=check_repo)) | |
150 | map.connect('files_raw_home', '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}', |
|
151 | map.connect('files_rawfile_home', '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}', | |
151 | controller='files', action='rawfile', revision='tip', f_path='', |
|
152 | controller='files', action='rawfile', revision='tip', f_path='', | |
152 | conditions=dict(function=check_repo)) |
|
153 | conditions=dict(function=check_repo)) | |
|
154 | map.connect('files_raw_home', '/{repo_name:.*}/raw/{revision}/{f_path:.*}', | |||
|
155 | controller='files', action='raw', revision='tip', f_path='', | |||
|
156 | conditions=dict(function=check_repo)) | |||
153 | map.connect('files_annotate_home', '/{repo_name:.*}/annotate/{revision}/{f_path:.*}', |
|
157 | map.connect('files_annotate_home', '/{repo_name:.*}/annotate/{revision}/{f_path:.*}', | |
154 | controller='files', action='annotate', revision='tip', f_path='', |
|
158 | controller='files', action='annotate', revision='tip', f_path='', | |
155 | conditions=dict(function=check_repo)) |
|
159 | conditions=dict(function=check_repo)) |
@@ -38,6 +38,7 b' from pylons_app.model.forms import UserF' | |||||
38 | ApplicationUiSettingsForm |
|
38 | ApplicationUiSettingsForm | |
39 | from pylons_app.model.hg_model import HgModel |
|
39 | from pylons_app.model.hg_model import HgModel | |
40 | from pylons_app.model.user_model import UserModel |
|
40 | from pylons_app.model.user_model import UserModel | |
|
41 | from pylons_app.lib.celerylib import tasks, run_task | |||
41 | import formencode |
|
42 | import formencode | |
42 | import logging |
|
43 | import logging | |
43 | import traceback |
|
44 | import traceback | |
@@ -102,6 +103,12 b' class SettingsController(BaseController)' | |||||
102 | invalidate_cache('cached_repo_list') |
|
103 | invalidate_cache('cached_repo_list') | |
103 | h.flash(_('Repositories sucessfully rescanned'), category='success') |
|
104 | h.flash(_('Repositories sucessfully rescanned'), category='success') | |
104 |
|
105 | |||
|
106 | if setting_id == 'whoosh': | |||
|
107 | repo_location = get_hg_ui_settings()['paths_root_path'] | |||
|
108 | full_index = request.POST.get('full_index', False) | |||
|
109 | task = run_task(tasks.whoosh_index, repo_location, full_index) | |||
|
110 | ||||
|
111 | h.flash(_('Whoosh reindex task scheduled'), category='success') | |||
105 | if setting_id == 'global': |
|
112 | if setting_id == 'global': | |
106 |
|
113 | |||
107 | application_form = ApplicationSettingsForm()() |
|
114 | application_form = ApplicationSettingsForm()() | |
@@ -253,7 +260,8 b' class SettingsController(BaseController)' | |||||
253 | # url('admin_settings_my_account_update', id=ID) |
|
260 | # url('admin_settings_my_account_update', id=ID) | |
254 | user_model = UserModel() |
|
261 | user_model = UserModel() | |
255 | uid = c.hg_app_user.user_id |
|
262 | uid = c.hg_app_user.user_id | |
256 |
_form = UserForm(edit=True, old_data={'user_id':uid |
|
263 | _form = UserForm(edit=True, old_data={'user_id':uid, | |
|
264 | 'email':c.hg_app_user.email})() | |||
257 | form_result = {} |
|
265 | form_result = {} | |
258 | try: |
|
266 | try: | |
259 | form_result = _form.to_python(dict(request.POST)) |
|
267 | form_result = _form.to_python(dict(request.POST)) | |
@@ -262,7 +270,11 b' class SettingsController(BaseController)' | |||||
262 | category='success') |
|
270 | category='success') | |
263 |
|
271 | |||
264 | except formencode.Invalid as errors: |
|
272 | except formencode.Invalid as errors: | |
265 |
|
|
273 | c.user = self.sa.query(User).get(c.hg_app_user.user_id) | |
|
274 | c.user_repos = [] | |||
|
275 | for repo in c.cached_repo_list.values(): | |||
|
276 | if repo.dbrepo.user.username == c.user.username: | |||
|
277 | c.user_repos.append(repo) | |||
266 | return htmlfill.render( |
|
278 | return htmlfill.render( | |
267 | render('admin/users/user_edit_my_account.html'), |
|
279 | render('admin/users/user_edit_my_account.html'), | |
268 | defaults=errors.value, |
|
280 | defaults=errors.value, |
@@ -98,7 +98,10 b' class UsersController(BaseController):' | |||||
98 | # method='put') |
|
98 | # method='put') | |
99 | # url('user', id=ID) |
|
99 | # url('user', id=ID) | |
100 | user_model = UserModel() |
|
100 | user_model = UserModel() | |
101 | _form = UserForm(edit=True, old_data={'user_id':id})() |
|
101 | c.user = user_model.get_user(id) | |
|
102 | ||||
|
103 | _form = UserForm(edit=True, old_data={'user_id':id, | |||
|
104 | 'email':c.user.email})() | |||
102 | form_result = {} |
|
105 | form_result = {} | |
103 | try: |
|
106 | try: | |
104 | form_result = _form.to_python(dict(request.POST)) |
|
107 | form_result = _form.to_python(dict(request.POST)) | |
@@ -106,7 +109,6 b' class UsersController(BaseController):' | |||||
106 | h.flash(_('User updated succesfully'), category='success') |
|
109 | h.flash(_('User updated succesfully'), category='success') | |
107 |
|
110 | |||
108 | except formencode.Invalid as errors: |
|
111 | except formencode.Invalid as errors: | |
109 | c.user = user_model.get_user(id) |
|
|||
110 | return htmlfill.render( |
|
112 | return htmlfill.render( | |
111 | render('admin/users/user_edit.html'), |
|
113 | render('admin/users/user_edit.html'), | |
112 | defaults=errors.value, |
|
114 | defaults=errors.value, | |
@@ -148,6 +150,8 b' class UsersController(BaseController):' | |||||
148 | """GET /users/id/edit: Form to edit an existing item""" |
|
150 | """GET /users/id/edit: Form to edit an existing item""" | |
149 | # url('edit_user', id=ID) |
|
151 | # url('edit_user', id=ID) | |
150 | c.user = self.sa.query(User).get(id) |
|
152 | c.user = self.sa.query(User).get(id) | |
|
153 | if not c.user: | |||
|
154 | return redirect(url('users')) | |||
151 | if c.user.username == 'default': |
|
155 | if c.user.username == 'default': | |
152 | h.flash(_("You can't edit this user since it's" |
|
156 | h.flash(_("You can't edit this user since it's" | |
153 | " crucial for entire application"), category='warning') |
|
157 | " crucial for entire application"), category='warning') |
@@ -45,6 +45,7 b' class FilesController(BaseController):' | |||||
45 | 'repository.admin') |
|
45 | 'repository.admin') | |
46 | def __before__(self): |
|
46 | def __before__(self): | |
47 | super(FilesController, self).__before__() |
|
47 | super(FilesController, self).__before__() | |
|
48 | c.file_size_limit = 250 * 1024 #limit of file size to display | |||
48 |
|
49 | |||
49 | def index(self, repo_name, revision, f_path): |
|
50 | def index(self, repo_name, revision, f_path): | |
50 | hg_model = HgModel() |
|
51 | hg_model = HgModel() | |
@@ -77,7 +78,6 b' class FilesController(BaseController):' | |||||
77 |
|
78 | |||
78 | c.changeset = repo.get_changeset(revision) |
|
79 | c.changeset = repo.get_changeset(revision) | |
79 |
|
80 | |||
80 |
|
||||
81 | c.cur_rev = c.changeset.raw_id |
|
81 | c.cur_rev = c.changeset.raw_id | |
82 | c.rev_nr = c.changeset.revision |
|
82 | c.rev_nr = c.changeset.revision | |
83 | c.files_list = c.changeset.get_node(f_path) |
|
83 | c.files_list = c.changeset.get_node(f_path) | |
@@ -97,6 +97,14 b' class FilesController(BaseController):' | |||||
97 | % f_path.split('/')[-1] |
|
97 | % f_path.split('/')[-1] | |
98 | return file_node.content |
|
98 | return file_node.content | |
99 |
|
99 | |||
|
100 | def raw(self, repo_name, revision, f_path): | |||
|
101 | hg_model = HgModel() | |||
|
102 | c.repo = hg_model.get_repo(c.repo_name) | |||
|
103 | file_node = c.repo.get_changeset(revision).get_node(f_path) | |||
|
104 | response.content_type = 'text/plain' | |||
|
105 | ||||
|
106 | return file_node.content | |||
|
107 | ||||
100 | def annotate(self, repo_name, revision, f_path): |
|
108 | def annotate(self, repo_name, revision, f_path): | |
101 | hg_model = HgModel() |
|
109 | hg_model = HgModel() | |
102 | c.repo = hg_model.get_repo(c.repo_name) |
|
110 | c.repo = hg_model.get_repo(c.repo_name) |
@@ -28,7 +28,9 b' from pylons import request, response, se' | |||||
28 | from pylons.controllers.util import abort, redirect |
|
28 | from pylons.controllers.util import abort, redirect | |
29 | from pylons_app.lib.auth import AuthUser, HasPermissionAnyDecorator |
|
29 | from pylons_app.lib.auth import AuthUser, HasPermissionAnyDecorator | |
30 | from pylons_app.lib.base import BaseController, render |
|
30 | from pylons_app.lib.base import BaseController, render | |
31 | from pylons_app.model.forms import LoginForm, RegisterForm |
|
31 | import pylons_app.lib.helpers as h | |
|
32 | from pylons.i18n.translation import _ | |||
|
33 | from pylons_app.model.forms import LoginForm, RegisterForm, PasswordResetForm | |||
32 | from pylons_app.model.user_model import UserModel |
|
34 | from pylons_app.model.user_model import UserModel | |
33 | import formencode |
|
35 | import formencode | |
34 | import logging |
|
36 | import logging | |
@@ -99,6 +101,8 b' class LoginController(BaseController):' | |||||
99 | form_result = register_form.to_python(dict(request.POST)) |
|
101 | form_result = register_form.to_python(dict(request.POST)) | |
100 | form_result['active'] = c.auto_active |
|
102 | form_result['active'] = c.auto_active | |
101 | user_model.create_registration(form_result) |
|
103 | user_model.create_registration(form_result) | |
|
104 | h.flash(_('You have successfully registered into hg-app'), | |||
|
105 | category='success') | |||
102 | return redirect(url('login_home')) |
|
106 | return redirect(url('login_home')) | |
103 |
|
107 | |||
104 | except formencode.Invalid as errors: |
|
108 | except formencode.Invalid as errors: | |
@@ -111,6 +115,28 b' class LoginController(BaseController):' | |||||
111 |
|
115 | |||
112 | return render('/register.html') |
|
116 | return render('/register.html') | |
113 |
|
117 | |||
|
118 | def password_reset(self): | |||
|
119 | user_model = UserModel() | |||
|
120 | if request.POST: | |||
|
121 | ||||
|
122 | password_reset_form = PasswordResetForm()() | |||
|
123 | try: | |||
|
124 | form_result = password_reset_form.to_python(dict(request.POST)) | |||
|
125 | user_model.reset_password(form_result) | |||
|
126 | h.flash(_('Your new password was sent'), | |||
|
127 | category='success') | |||
|
128 | return redirect(url('login_home')) | |||
|
129 | ||||
|
130 | except formencode.Invalid as errors: | |||
|
131 | return htmlfill.render( | |||
|
132 | render('/password_reset.html'), | |||
|
133 | defaults=errors.value, | |||
|
134 | errors=errors.error_dict or {}, | |||
|
135 | prefix_error=False, | |||
|
136 | encoding="UTF-8") | |||
|
137 | ||||
|
138 | return render('/password_reset.html') | |||
|
139 | ||||
114 | def logout(self): |
|
140 | def logout(self): | |
115 | session['hg_app_user'] = AuthUser() |
|
141 | session['hg_app_user'] = AuthUser() | |
116 | session.save() |
|
142 | session.save() |
@@ -26,10 +26,9 b' from pylons import request, response, se' | |||||
26 | from pylons.controllers.util import abort, redirect |
|
26 | from pylons.controllers.util import abort, redirect | |
27 | from pylons_app.lib.auth import LoginRequired |
|
27 | from pylons_app.lib.auth import LoginRequired | |
28 | from pylons_app.lib.base import BaseController, render |
|
28 | from pylons_app.lib.base import BaseController, render | |
29 |
from pylons_app.lib.indexers import |
|
29 | from pylons_app.lib.indexers import IDX_LOCATION, SCHEMA, IDX_NAME, ResultWrapper | |
30 |
from webhelpers. |
|
30 | from webhelpers.paginate import Page | |
31 | from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter, \ |
|
31 | from webhelpers.util import update_params | |
32 | ContextFragmenter |
|
|||
33 | from pylons.i18n.translation import _ |
|
32 | from pylons.i18n.translation import _ | |
34 | from whoosh.index import open_dir, EmptyIndexError |
|
33 | from whoosh.index import open_dir, EmptyIndexError | |
35 | from whoosh.qparser import QueryParser, QueryParserError |
|
34 | from whoosh.qparser import QueryParser, QueryParserError | |
@@ -45,17 +44,16 b' class SearchController(BaseController):' | |||||
45 | def __before__(self): |
|
44 | def __before__(self): | |
46 | super(SearchController, self).__before__() |
|
45 | super(SearchController, self).__before__() | |
47 |
|
46 | |||
48 |
|
||||
49 | def index(self): |
|
47 | def index(self): | |
50 | c.formated_results = [] |
|
48 | c.formated_results = [] | |
51 | c.runtime = '' |
|
49 | c.runtime = '' | |
52 | search_items = set() |
|
|||
53 | c.cur_query = request.GET.get('q', None) |
|
50 | c.cur_query = request.GET.get('q', None) | |
54 | if c.cur_query: |
|
51 | if c.cur_query: | |
55 | cur_query = c.cur_query.lower() |
|
52 | cur_query = c.cur_query.lower() | |
56 |
|
53 | |||
57 |
|
||||
58 | if c.cur_query: |
|
54 | if c.cur_query: | |
|
55 | p = int(request.params.get('page', 1)) | |||
|
56 | highlight_items = set() | |||
59 | try: |
|
57 | try: | |
60 | idx = open_dir(IDX_LOCATION, indexname=IDX_NAME) |
|
58 | idx = open_dir(IDX_LOCATION, indexname=IDX_NAME) | |
61 | searcher = idx.searcher() |
|
59 | searcher = idx.searcher() | |
@@ -65,49 +63,36 b' class SearchController(BaseController):' | |||||
65 | query = qp.parse(unicode(cur_query)) |
|
63 | query = qp.parse(unicode(cur_query)) | |
66 |
|
64 | |||
67 | if isinstance(query, Phrase): |
|
65 | if isinstance(query, Phrase): | |
68 |
|
|
66 | highlight_items.update(query.words) | |
69 | else: |
|
67 | else: | |
70 | for i in query.all_terms(): |
|
68 | for i in query.all_terms(): | |
71 |
|
|
69 | if i[0] == 'content': | |
|
70 | highlight_items.add(i[1]) | |||
|
71 | ||||
|
72 | matcher = query.matcher(searcher) | |||
72 |
|
|
73 | ||
73 | log.debug(query) |
|
74 | log.debug(query) | |
74 |
log.debug( |
|
75 | log.debug(highlight_items) | |
75 | results = searcher.search(query) |
|
76 | results = searcher.search(query) | |
|
77 | res_ln = len(results) | |||
76 | c.runtime = '%s results (%.3f seconds)' \ |
|
78 | c.runtime = '%s results (%.3f seconds)' \ | |
77 |
% ( |
|
79 | % (res_ln, results.runtime) | |
78 |
|
||||
79 | analyzer = ANALYZER |
|
|||
80 | formatter = HtmlFormatter('span', |
|
|||
81 | between='\n<span class="break">...</span>\n') |
|
|||
82 |
|
||||
83 | #how the parts are splitted within the same text part |
|
|||
84 | fragmenter = SimpleFragmenter(200) |
|
|||
85 | #fragmenter = ContextFragmenter(search_items) |
|
|||
86 |
|
80 | |||
87 | for res in results: |
|
81 | def url_generator(**kw): | |
88 | d = {} |
|
82 | return update_params("?q=%s" % c.cur_query, **kw) | |
89 | d.update(res) |
|
83 | ||
90 | hl = highlight(escape(res['content']), search_items, |
|
84 | c.formated_results = Page( | |
91 | analyzer=analyzer, |
|
85 | ResultWrapper(searcher, matcher, highlight_items), | |
92 | fragmenter=fragmenter, |
|
86 | page=p, item_count=res_ln, | |
93 | formatter=formatter, |
|
87 | items_per_page=10, url=url_generator) | |
94 | top=5) |
|
|||
95 | f_path = res['path'][res['path'].find(res['repository']) \ |
|
|||
96 | + len(res['repository']):].lstrip('/') |
|
|||
97 | d.update({'content_short':hl, |
|
|||
98 | 'f_path':f_path}) |
|
|||
99 | #del d['content'] |
|
|||
100 | c.formated_results.append(d) |
|
|||
101 |
|
|
88 | ||
102 | except QueryParserError: |
|
89 | except QueryParserError: | |
103 | c.runtime = _('Invalid search query. Try quoting it.') |
|
90 | c.runtime = _('Invalid search query. Try quoting it.') | |
104 |
|
91 | searcher.close() | ||
105 | except (EmptyIndexError, IOError): |
|
92 | except (EmptyIndexError, IOError): | |
106 | log.error(traceback.format_exc()) |
|
93 | log.error(traceback.format_exc()) | |
107 | log.error('Empty Index data') |
|
94 | log.error('Empty Index data') | |
108 | c.runtime = _('There is no index to search in. Please run whoosh indexer') |
|
95 | c.runtime = _('There is no index to search in. Please run whoosh indexer') | |
109 |
|
96 | |||
110 |
|
||||
111 |
|
||||
112 | # Return a rendered template |
|
97 | # Return a rendered template | |
113 | return render('/search/search.html') |
|
98 | return render('/search/search.html') |
@@ -22,15 +22,17 b' 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 datetime import datetime, timedelta |
|
25 | from pylons import tmpl_context as c, request, url | |
26 | from pylons import tmpl_context as c, request |
|
|||
27 | from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
26 | from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |
28 | from pylons_app.lib.base import BaseController, render |
|
27 | from pylons_app.lib.base import BaseController, render | |
29 | from pylons_app.lib.helpers import person |
|
|||
30 | from pylons_app.lib.utils import OrderedDict |
|
28 | from pylons_app.lib.utils import OrderedDict | |
31 | 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 | |||
|
31 | from webhelpers.paginate import Page | |||
|
32 | from pylons_app.lib.celerylib import run_task | |||
|
33 | from pylons_app.lib.celerylib.tasks import get_commits_stats | |||
|
34 | from datetime import datetime, timedelta | |||
32 | from time import mktime |
|
35 | from time import mktime | |
33 | from webhelpers.paginate import Page |
|
|||
34 | import calendar |
|
36 | import calendar | |
35 | import logging |
|
37 | import logging | |
36 |
|
38 | |||
@@ -63,77 +65,32 b' class SummaryController(BaseController):' | |||||
63 | for name, hash in c.repo_info.branches.items()[:10]: |
|
65 | for name, hash in c.repo_info.branches.items()[:10]: | |
64 | c.repo_branches[name] = c.repo_info.get_changeset(hash) |
|
66 | c.repo_branches[name] = c.repo_info.get_changeset(hash) | |
65 |
|
67 | |||
66 | c.commit_data = self.__get_commit_stats(c.repo_info) |
|
68 | td = datetime.today() + timedelta(days=1) | |
|
69 | y, m, d = td.year, td.month, td.day | |||
|
70 | ||||
|
71 | ts_min_y = mktime((y - 1, (td - timedelta(days=calendar.mdays[m])).month, | |||
|
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] ]) | |||
67 |
|
94 | |||
68 | return render('summary/summary.html') |
|
95 | return render('summary/summary.html') | |
69 |
|
96 | |||
70 |
|
||||
71 |
|
||||
72 | def __get_commit_stats(self, repo): |
|
|||
73 | aggregate = OrderedDict() |
|
|||
74 |
|
||||
75 | #graph range |
|
|||
76 | td = datetime.today() + timedelta(days=1) |
|
|||
77 | y, m, d = td.year, td.month, td.day |
|
|||
78 | c.ts_min = mktime((y, (td - timedelta(days=calendar.mdays[m])).month, |
|
|||
79 | d, 0, 0, 0, 0, 0, 0,)) |
|
|||
80 | c.ts_max = mktime((y, m, d, 0, 0, 0, 0, 0, 0,)) |
|
|||
81 |
|
||||
82 | def author_key_cleaner(k): |
|
|||
83 | k = person(k) |
|
|||
84 | k = k.replace('"', "'") #for js data compatibilty |
|
|||
85 | return k |
|
|||
86 |
|
||||
87 | for cs in repo[:200]:#added limit 200 until fix #29 is made |
|
|||
88 | k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1], |
|
|||
89 | cs.date.timetuple()[2]) |
|
|||
90 | timetupple = [int(x) for x in k.split('-')] |
|
|||
91 | timetupple.extend([0 for _ in xrange(6)]) |
|
|||
92 | k = mktime(timetupple) |
|
|||
93 | if aggregate.has_key(author_key_cleaner(cs.author)): |
|
|||
94 | if aggregate[author_key_cleaner(cs.author)].has_key(k): |
|
|||
95 | aggregate[author_key_cleaner(cs.author)][k]["commits"] += 1 |
|
|||
96 | aggregate[author_key_cleaner(cs.author)][k]["added"] += len(cs.added) |
|
|||
97 | aggregate[author_key_cleaner(cs.author)][k]["changed"] += len(cs.changed) |
|
|||
98 | aggregate[author_key_cleaner(cs.author)][k]["removed"] += len(cs.removed) |
|
|||
99 |
|
||||
100 | else: |
|
|||
101 | #aggregate[author_key_cleaner(cs.author)].update(dates_range) |
|
|||
102 | if k >= c.ts_min and k <= c.ts_max: |
|
|||
103 | aggregate[author_key_cleaner(cs.author)][k] = {} |
|
|||
104 | aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1 |
|
|||
105 | aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added) |
|
|||
106 | aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed) |
|
|||
107 | aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed) |
|
|||
108 |
|
||||
109 | else: |
|
|||
110 | if k >= c.ts_min and k <= c.ts_max: |
|
|||
111 | aggregate[author_key_cleaner(cs.author)] = OrderedDict() |
|
|||
112 | #aggregate[author_key_cleaner(cs.author)].update(dates_range) |
|
|||
113 | aggregate[author_key_cleaner(cs.author)][k] = {} |
|
|||
114 | aggregate[author_key_cleaner(cs.author)][k]["commits"] = 1 |
|
|||
115 | aggregate[author_key_cleaner(cs.author)][k]["added"] = len(cs.added) |
|
|||
116 | aggregate[author_key_cleaner(cs.author)][k]["changed"] = len(cs.changed) |
|
|||
117 | aggregate[author_key_cleaner(cs.author)][k]["removed"] = len(cs.removed) |
|
|||
118 |
|
||||
119 | d = '' |
|
|||
120 | tmpl0 = u""""%s":%s""" |
|
|||
121 | tmpl1 = u"""{label:"%s",data:%s,schema:["commits"]},""" |
|
|||
122 | for author in aggregate: |
|
|||
123 |
|
||||
124 | d += tmpl0 % (author, |
|
|||
125 | tmpl1 \ |
|
|||
126 | % (author, |
|
|||
127 | [{"time":x, |
|
|||
128 | "commits":aggregate[author][x]['commits'], |
|
|||
129 | "added":aggregate[author][x]['added'], |
|
|||
130 | "changed":aggregate[author][x]['changed'], |
|
|||
131 | "removed":aggregate[author][x]['removed'], |
|
|||
132 | } for x in aggregate[author]])) |
|
|||
133 | if d == '': |
|
|||
134 | d = '"%s":{label:"%s",data:[[0,1],]}' \ |
|
|||
135 | % (author_key_cleaner(repo.contact), |
|
|||
136 | author_key_cleaner(repo.contact)) |
|
|||
137 | return d |
|
|||
138 |
|
||||
139 |
|
@@ -34,9 +34,36 b' from sqlalchemy.orm.exc import NoResultF' | |||||
34 | import bcrypt |
|
34 | import bcrypt | |
35 | from decorator import decorator |
|
35 | from decorator import decorator | |
36 | import logging |
|
36 | import logging | |
|
37 | import random | |||
37 |
|
38 | |||
38 | log = logging.getLogger(__name__) |
|
39 | log = logging.getLogger(__name__) | |
39 |
|
40 | |||
|
41 | class PasswordGenerator(object): | |||
|
42 | """This is a simple class for generating password from | |||
|
43 | different sets of characters | |||
|
44 | usage: | |||
|
45 | passwd_gen = PasswordGenerator() | |||
|
46 | #print 8-letter password containing only big and small letters of alphabet | |||
|
47 | print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL) | |||
|
48 | """ | |||
|
49 | ALPHABETS_NUM = r'''1234567890'''#[0] | |||
|
50 | ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''#[1] | |||
|
51 | ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''#[2] | |||
|
52 | ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?''' #[3] | |||
|
53 | ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM + ALPHABETS_SPECIAL#[4] | |||
|
54 | ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM#[5] | |||
|
55 | ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL | |||
|
56 | ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6] | |||
|
57 | ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7] | |||
|
58 | ||||
|
59 | def __init__(self, passwd=''): | |||
|
60 | self.passwd = passwd | |||
|
61 | ||||
|
62 | def gen_password(self, len, type): | |||
|
63 | self.passwd = ''.join([random.choice(type) for _ in xrange(len)]) | |||
|
64 | return self.passwd | |||
|
65 | ||||
|
66 | ||||
40 | def get_crypt_password(password): |
|
67 | def get_crypt_password(password): | |
41 | """Cryptographic function used for password hashing based on sha1 |
|
68 | """Cryptographic function used for password hashing based on sha1 | |
42 | @param password: password to hash |
|
69 | @param password: password to hash |
@@ -84,11 +84,13 b' class DbManage(object):' | |||||
84 | import getpass |
|
84 | import getpass | |
85 | username = raw_input('Specify admin username:') |
|
85 | username = raw_input('Specify admin username:') | |
86 | password = getpass.getpass('Specify admin password:') |
|
86 | password = getpass.getpass('Specify admin password:') | |
87 | self.create_user(username, password, True) |
|
87 | email = raw_input('Specify admin email:') | |
|
88 | self.create_user(username, password, email, True) | |||
88 | else: |
|
89 | else: | |
89 | log.info('creating admin and regular test users') |
|
90 | log.info('creating admin and regular test users') | |
90 | self.create_user('test_admin', 'test', True) |
|
91 | self.create_user('test_admin', 'test', 'test_admin@mail.com', True) | |
91 | self.create_user('test_regular', 'test', False) |
|
92 | self.create_user('test_regular', 'test', 'test_regular@mail.com', False) | |
|
93 | self.create_user('test_regular2', 'test', 'test_regular2@mail.com', False) | |||
92 |
|
94 | |||
93 |
|
95 | |||
94 |
|
96 | |||
@@ -166,14 +168,14 b' class DbManage(object):' | |||||
166 | raise |
|
168 | raise | |
167 | log.info('created ui config') |
|
169 | log.info('created ui config') | |
168 |
|
170 | |||
169 | def create_user(self, username, password, admin=False): |
|
171 | def create_user(self, username, password, email='', admin=False): | |
170 | log.info('creating administrator user %s', username) |
|
172 | log.info('creating administrator user %s', username) | |
171 | new_user = User() |
|
173 | new_user = User() | |
172 | new_user.username = username |
|
174 | new_user.username = username | |
173 | new_user.password = get_crypt_password(password) |
|
175 | new_user.password = get_crypt_password(password) | |
174 | new_user.name = 'Hg' |
|
176 | new_user.name = 'Hg' | |
175 | new_user.lastname = 'Admin' |
|
177 | new_user.lastname = 'Admin' | |
176 |
new_user.email = |
|
178 | new_user.email = email | |
177 | new_user.admin = admin |
|
179 | new_user.admin = admin | |
178 | new_user.active = True |
|
180 | new_user.active = True | |
179 |
|
181 |
@@ -277,13 +277,17 b' def pygmentize_annotation(filenode, **kw' | |||||
277 | return literal(annotate_highlight(filenode, url_func, **kwargs)) |
|
277 | return literal(annotate_highlight(filenode, url_func, **kwargs)) | |
278 |
|
278 | |||
279 | def repo_name_slug(value): |
|
279 | def repo_name_slug(value): | |
|
280 | """Return slug of name of repository | |||
|
281 | This function is called on each creation/modification | |||
|
282 | of repository to prevent bad names in repo | |||
280 |
|
|
283 | """ | |
281 | Return slug of name of repository |
|
284 | slug = remove_formatting(value) | |
282 | """ |
|
285 | slug = strip_tags(slug) | |
283 | slug = urlify(value) |
|
286 | ||
284 | for c in """=[]\;'"<>,/~!@#$%^&*()+{}|:""": |
|
287 | for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """: | |
285 | slug = slug.replace(c, '-') |
|
288 | slug = slug.replace(c, '-') | |
286 | slug = recursive_replace(slug, '-') |
|
289 | slug = recursive_replace(slug, '-') | |
|
290 | slug = collapse(slug, '-') | |||
287 | return slug |
|
291 | return slug | |
288 |
|
292 | |||
289 | def get_changeset_safe(repo, rev): |
|
293 | def get_changeset_safe(repo, rev): | |
@@ -321,6 +325,7 b" isodate = lambda x: util.datestr(x, '%Y" | |||||
321 | isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2') |
|
325 | isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2') | |
322 | localdate = lambda x: (x[0], util.makedate()[1]) |
|
326 | localdate = lambda x: (x[0], util.makedate()[1]) | |
323 | rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2") |
|
327 | rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2") | |
|
328 | rfc822date_notz = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S") | |||
324 | rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2") |
|
329 | rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2") | |
325 | time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2") |
|
330 | time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2") | |
326 |
|
331 |
@@ -1,28 +1,31 b'' | |||||
1 | import sys |
|
1 | from os.path import dirname as dn, join as jn | |
|
2 | from pylons_app.config.environment import load_environment | |||
|
3 | from pylons_app.model.hg_model import HgModel | |||
|
4 | from shutil import rmtree | |||
|
5 | from webhelpers.html.builder import escape | |||
|
6 | from vcs.utils.lazy import LazyProperty | |||
|
7 | ||||
|
8 | from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter | |||
|
9 | from whoosh.fields import TEXT, ID, STORED, Schema, FieldType | |||
|
10 | from whoosh.index import create_in, open_dir | |||
|
11 | from whoosh.formats import Characters | |||
|
12 | from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter | |||
|
13 | ||||
2 | import os |
|
14 | import os | |
3 | from pidlock import LockHeld, DaemonLock |
|
15 | import sys | |
4 | import traceback |
|
16 | import traceback | |
5 |
|
17 | |||
6 | from os.path import dirname as dn |
|
|||
7 | from os.path import join as jn |
|
|||
8 |
|
||||
9 | #to get the pylons_app import |
|
18 | #to get the pylons_app import | |
10 | sys.path.append(dn(dn(dn(os.path.realpath(__file__))))) |
|
19 | sys.path.append(dn(dn(dn(os.path.realpath(__file__))))) | |
11 |
|
20 | |||
12 | from pylons_app.config.environment import load_environment |
|
|||
13 | from pylons_app.model.hg_model import HgModel |
|
|||
14 | from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter |
|
|||
15 | from whoosh.fields import TEXT, ID, STORED, Schema |
|
|||
16 | from whoosh.index import create_in, open_dir |
|
|||
17 | from shutil import rmtree |
|
|||
18 |
|
21 | |||
19 | #LOCATION WE KEEP THE INDEX |
|
22 | #LOCATION WE KEEP THE INDEX | |
20 | IDX_LOCATION = jn(dn(dn(dn(dn(os.path.abspath(__file__))))), 'data', 'index') |
|
23 | IDX_LOCATION = jn(dn(dn(dn(dn(os.path.abspath(__file__))))), 'data', 'index') | |
21 |
|
24 | |||
22 | #EXTENSIONS WE WANT TO INDEX CONTENT OFF |
|
25 | #EXTENSIONS WE WANT TO INDEX CONTENT OFF | |
23 |
INDEX_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c', |
|
26 | INDEX_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c', | |
24 |
'cfm', 'cpp', 'cs', 'css', 'diff', 'do', 'el', 'erl', |
|
27 | 'cfg', 'cfm', 'cpp', 'cs', 'css', 'diff', 'do', 'el', 'erl', | |
25 |
'htm', 'html', 'ini', 'java', 'js', 'jsp', 'jspx', 'lisp', |
|
28 | 'h', 'htm', 'html', 'ini', 'java', 'js', 'jsp', 'jspx', 'lisp', | |
26 |
'lua', 'm', 'mako', 'ml', 'pas', 'patch', 'php', 'php3', |
|
29 | 'lua', 'm', 'mako', 'ml', 'pas', 'patch', 'php', 'php3', | |
27 |
'php4', 'phtml', 'pm', 'py', 'rb', 'rst', 's', 'sh', 'sql', |
|
30 | 'php4', 'phtml', 'pm', 'py', 'rb', 'rst', 's', 'sh', 'sql', | |
28 |
'tpl', 'txt', 'vim', 'wss', 'xhtml', 'xml','xsl','xslt', |
|
31 | 'tpl', 'txt', 'vim', 'wss', 'xhtml', 'xml', 'xsl', 'xslt', | |
@@ -31,11 +34,106 b" INDEX_EXTENSIONS = ['action', 'adp', 'as" | |||||
31 | #CUSTOM ANALYZER wordsplit + lowercase filter |
|
34 | #CUSTOM ANALYZER wordsplit + lowercase filter | |
32 | ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter() |
|
35 | ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter() | |
33 |
|
36 | |||
|
37 | ||||
34 | #INDEX SCHEMA DEFINITION |
|
38 | #INDEX SCHEMA DEFINITION | |
35 | SCHEMA = Schema(owner=TEXT(), |
|
39 | SCHEMA = Schema(owner=TEXT(), | |
36 | repository=TEXT(stored=True), |
|
40 | repository=TEXT(stored=True), | |
37 | path=ID(stored=True, unique=True), |
|
41 | path=ID(stored=True, unique=True), | |
38 |
content= |
|
42 | content=FieldType(format=Characters(ANALYZER), | |
|
43 | scorable=True, stored=True), | |||
39 | modtime=STORED(),extension=TEXT(stored=True)) |
|
44 | modtime=STORED(), extension=TEXT(stored=True)) | |
40 |
|
45 | |||
41 | IDX_NAME = 'HG_INDEX' No newline at end of file |
|
46 | ||
|
47 | IDX_NAME = 'HG_INDEX' | |||
|
48 | FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n') | |||
|
49 | FRAGMENTER = SimpleFragmenter(200) | |||
|
50 | ||||
|
51 | class ResultWrapper(object): | |||
|
52 | def __init__(self, searcher, matcher, highlight_items): | |||
|
53 | self.searcher = searcher | |||
|
54 | self.matcher = matcher | |||
|
55 | self.highlight_items = highlight_items | |||
|
56 | self.fragment_size = 200 / 2 | |||
|
57 | ||||
|
58 | @LazyProperty | |||
|
59 | def doc_ids(self): | |||
|
60 | docs_id = [] | |||
|
61 | while self.matcher.is_active(): | |||
|
62 | docnum = self.matcher.id() | |||
|
63 | chunks = [offsets for offsets in self.get_chunks()] | |||
|
64 | docs_id.append([docnum, chunks]) | |||
|
65 | self.matcher.next() | |||
|
66 | return docs_id | |||
|
67 | ||||
|
68 | def __str__(self): | |||
|
69 | return '<%s at %s>' % (self.__class__.__name__, len(self.doc_ids)) | |||
|
70 | ||||
|
71 | def __repr__(self): | |||
|
72 | return self.__str__() | |||
|
73 | ||||
|
74 | def __len__(self): | |||
|
75 | return len(self.doc_ids) | |||
|
76 | ||||
|
77 | def __iter__(self): | |||
|
78 | """ | |||
|
79 | Allows Iteration over results,and lazy generate content | |||
|
80 | ||||
|
81 | *Requires* implementation of ``__getitem__`` method. | |||
|
82 | """ | |||
|
83 | for docid in self.doc_ids: | |||
|
84 | yield self.get_full_content(docid) | |||
|
85 | ||||
|
86 | def __getslice__(self, i, j): | |||
|
87 | """ | |||
|
88 | Slicing of resultWrapper | |||
|
89 | """ | |||
|
90 | slice = [] | |||
|
91 | for docid in self.doc_ids[i:j]: | |||
|
92 | slice.append(self.get_full_content(docid)) | |||
|
93 | return slice | |||
|
94 | ||||
|
95 | ||||
|
96 | def get_full_content(self, docid): | |||
|
97 | res = self.searcher.stored_fields(docid[0]) | |||
|
98 | f_path = res['path'][res['path'].find(res['repository']) \ | |||
|
99 | + len(res['repository']):].lstrip('/') | |||
|
100 | ||||
|
101 | content_short = self.get_short_content(res, docid[1]) | |||
|
102 | res.update({'content_short':content_short, | |||
|
103 | 'content_short_hl':self.highlight(content_short), | |||
|
104 | 'f_path':f_path}) | |||
|
105 | ||||
|
106 | return res | |||
|
107 | ||||
|
108 | def get_short_content(self, res, chunks): | |||
|
109 | ||||
|
110 | return ''.join([res['content'][chunk[0]:chunk[1]] for chunk in chunks]) | |||
|
111 | ||||
|
112 | def get_chunks(self): | |||
|
113 | """ | |||
|
114 | Smart function that implements chunking the content | |||
|
115 | but not overlap chunks so it doesn't highlight the same | |||
|
116 | close occurences twice. | |||
|
117 | @param matcher: | |||
|
118 | @param size: | |||
|
119 | """ | |||
|
120 | memory = [(0, 0)] | |||
|
121 | for span in self.matcher.spans(): | |||
|
122 | start = span.startchar or 0 | |||
|
123 | end = span.endchar or 0 | |||
|
124 | start_offseted = max(0, start - self.fragment_size) | |||
|
125 | end_offseted = end + self.fragment_size | |||
|
126 | ||||
|
127 | if start_offseted < memory[-1][1]: | |||
|
128 | start_offseted = memory[-1][1] | |||
|
129 | memory.append((start_offseted, end_offseted,)) | |||
|
130 | yield (start_offseted, end_offseted,) | |||
|
131 | ||||
|
132 | def highlight(self, content, top=5): | |||
|
133 | hl = highlight(escape(content), | |||
|
134 | self.highlight_items, | |||
|
135 | analyzer=ANALYZER, | |||
|
136 | fragmenter=FRAGMENTER, | |||
|
137 | formatter=FORMATTER, | |||
|
138 | top=top) | |||
|
139 | return hl |
@@ -32,20 +32,31 b' from os.path import join as jn' | |||||
32 | project_path = dn(dn(dn(dn(os.path.realpath(__file__))))) |
|
32 | project_path = dn(dn(dn(dn(os.path.realpath(__file__))))) | |
33 | sys.path.append(project_path) |
|
33 | sys.path.append(project_path) | |
34 |
|
34 | |||
35 | from pidlock import LockHeld, DaemonLock |
|
35 | from pylons_app.lib.pidlock import LockHeld, DaemonLock | |
36 | import traceback |
|
|||
37 | from pylons_app.config.environment import load_environment |
|
|||
38 | from pylons_app.model.hg_model import HgModel |
|
36 | from pylons_app.model.hg_model import HgModel | |
39 | from pylons_app.lib.helpers import safe_unicode |
|
37 | from pylons_app.lib.helpers import safe_unicode | |
40 | from whoosh.index import create_in, open_dir |
|
38 | from whoosh.index import create_in, open_dir | |
41 | from shutil import rmtree |
|
39 | from shutil import rmtree | |
42 |
from pylons_app.lib.indexers import |
|
40 | from pylons_app.lib.indexers import INDEX_EXTENSIONS, IDX_LOCATION, SCHEMA, IDX_NAME | |
43 | SCHEMA, IDX_NAME |
|
|||
44 |
|
41 | |||
45 | import logging |
|
42 | import logging | |
46 | import logging.config |
|
43 | ||
47 | logging.config.fileConfig(jn(project_path, 'development.ini')) |
|
|||
48 | log = logging.getLogger('whooshIndexer') |
|
44 | log = logging.getLogger('whooshIndexer') | |
|
45 | # create logger | |||
|
46 | log.setLevel(logging.DEBUG) | |||
|
47 | log.propagate = False | |||
|
48 | # create console handler and set level to debug | |||
|
49 | ch = logging.StreamHandler() | |||
|
50 | ch.setLevel(logging.DEBUG) | |||
|
51 | ||||
|
52 | # create formatter | |||
|
53 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |||
|
54 | ||||
|
55 | # add formatter to ch | |||
|
56 | ch.setFormatter(formatter) | |||
|
57 | ||||
|
58 | # add ch to logger | |||
|
59 | log.addHandler(ch) | |||
49 |
|
60 | |||
50 | def scan_paths(root_location): |
|
61 | def scan_paths(root_location): | |
51 | return HgModel.repo_scan('/', root_location, None, True) |
|
62 | return HgModel.repo_scan('/', root_location, None, True) | |
@@ -221,6 +232,7 b' if __name__ == "__main__":' | |||||
221 | WhooshIndexingDaemon(repo_location=repo_location)\ |
|
232 | WhooshIndexingDaemon(repo_location=repo_location)\ | |
222 | .run(full_index=full_index) |
|
233 | .run(full_index=full_index) | |
223 | l.release() |
|
234 | l.release() | |
|
235 | reload(logging) | |||
224 | except LockHeld: |
|
236 | except LockHeld: | |
225 | sys.exit(1) |
|
237 | sys.exit(1) | |
226 |
|
238 |
@@ -6,7 +6,7 b' class LockHeld(Exception):pass' | |||||
6 |
|
6 | |||
7 |
|
7 | |||
8 | class DaemonLock(object): |
|
8 | class DaemonLock(object): | |
9 |
|
|
9 | """daemon locking | |
10 | USAGE: |
|
10 | USAGE: | |
11 | try: |
|
11 | try: | |
12 | l = lock() |
|
12 | l = lock() | |
@@ -14,7 +14,7 b' class DaemonLock(object):' | |||||
14 | l.release() |
|
14 | l.release() | |
15 | except LockHeld: |
|
15 | except LockHeld: | |
16 | sys.exit(1) |
|
16 | sys.exit(1) | |
17 | ''' |
|
17 | """ | |
18 |
|
18 | |||
19 | def __init__(self, file=None, callbackfn=None, |
|
19 | def __init__(self, file=None, callbackfn=None, | |
20 | desc='daemon lock', debug=False): |
|
20 | desc='daemon lock', debug=False): | |
@@ -40,9 +40,9 b' class DaemonLock(object):' | |||||
40 |
|
40 | |||
41 |
|
41 | |||
42 | def lock(self): |
|
42 | def lock(self): | |
43 |
|
|
43 | """ | |
44 | locking function, if lock is present it will raise LockHeld exception |
|
44 | locking function, if lock is present it will raise LockHeld exception | |
45 |
|
|
45 | """ | |
46 | lockname = '%s' % (os.getpid()) |
|
46 | lockname = '%s' % (os.getpid()) | |
47 |
|
47 | |||
48 | self.trylock() |
|
48 | self.trylock() | |
@@ -75,9 +75,9 b' class DaemonLock(object):' | |||||
75 |
|
75 | |||
76 |
|
76 | |||
77 | def release(self): |
|
77 | def release(self): | |
78 |
|
|
78 | """ | |
79 | releases the pid by removing the pidfile |
|
79 | releases the pid by removing the pidfile | |
80 |
|
|
80 | """ | |
81 | if self.callbackfn: |
|
81 | if self.callbackfn: | |
82 | #execute callback function on release |
|
82 | #execute callback function on release | |
83 | if self.debug: |
|
83 | if self.debug: | |
@@ -94,11 +94,11 b' class DaemonLock(object):' | |||||
94 | pass |
|
94 | pass | |
95 |
|
95 | |||
96 | def makelock(self, lockname, pidfile): |
|
96 | def makelock(self, lockname, pidfile): | |
97 |
|
|
97 | """ | |
98 | this function will make an actual lock |
|
98 | this function will make an actual lock | |
99 | @param lockname: acctual pid of file |
|
99 | @param lockname: acctual pid of file | |
100 | @param pidfile: the file to write the pid in |
|
100 | @param pidfile: the file to write the pid in | |
101 |
|
|
101 | """ | |
102 | if self.debug: |
|
102 | if self.debug: | |
103 | print 'creating a file %s and pid: %s' % (pidfile, lockname) |
|
103 | print 'creating a file %s and pid: %s' % (pidfile, lockname) | |
104 | pidfile = open(self.pidfile, "wb") |
|
104 | pidfile = open(self.pidfile, "wb") |
@@ -1,7 +1,6 b'' | |||||
1 | from sqlalchemy.interfaces import ConnectionProxy |
|
1 | from sqlalchemy.interfaces import ConnectionProxy | |
2 | import time |
|
2 | import time | |
3 | import logging |
|
3 | from sqlalchemy import log | |
4 | log = logging.getLogger('timerproxy') |
|
|||
5 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38) |
|
4 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38) | |
6 |
|
5 | |||
7 | def color_sql(sql): |
|
6 | def color_sql(sql): | |
@@ -39,19 +38,22 b' def format_sql(sql):' | |||||
39 |
|
38 | |||
40 |
|
39 | |||
41 | class TimerProxy(ConnectionProxy): |
|
40 | class TimerProxy(ConnectionProxy): | |
|
41 | ||||
|
42 | def __init__(self): | |||
|
43 | super(TimerProxy, self).__init__() | |||
|
44 | self.logging_name = 'timerProxy' | |||
|
45 | self.log = log.instance_logger(self, True) | |||
|
46 | ||||
42 | def cursor_execute(self, execute, cursor, statement, parameters, context, executemany): |
|
47 | def cursor_execute(self, execute, cursor, statement, parameters, context, executemany): | |
|
48 | ||||
43 | now = time.time() |
|
49 | now = time.time() | |
44 | try: |
|
50 | try: | |
45 | log.info(">>>>> STARTING QUERY >>>>>") |
|
51 | self.log.info(">>>>> STARTING QUERY >>>>>") | |
46 | return execute(cursor, statement, parameters, context) |
|
52 | return execute(cursor, statement, parameters, context) | |
47 | finally: |
|
53 | finally: | |
48 | total = time.time() - now |
|
54 | total = time.time() - now | |
49 | try: |
|
55 | try: | |
50 | log.info(format_sql("Query: %s" % statement % parameters)) |
|
56 | self.log.info(format_sql("Query: %s" % statement % parameters)) | |
51 | except TypeError: |
|
57 | except TypeError: | |
52 | log.info(format_sql("Query: %s %s" % (statement, parameters))) |
|
58 | self.log.info(format_sql("Query: %s %s" % (statement, parameters))) | |
53 | log.info("<<<<< TOTAL TIME: %f <<<<<" % total) |
|
59 | self.log.info("<<<<< TOTAL TIME: %f <<<<<" % total) | |
54 |
|
||||
55 |
|
||||
56 |
|
||||
57 |
|
@@ -31,6 +31,7 b' from vcs.backends.base import BaseChange' | |||||
31 | from vcs.utils.lazy import LazyProperty |
|
31 | from vcs.utils.lazy import LazyProperty | |
32 | import logging |
|
32 | import logging | |
33 | import os |
|
33 | import os | |
|
34 | ||||
34 | log = logging.getLogger(__name__) |
|
35 | log = logging.getLogger(__name__) | |
35 |
|
36 | |||
36 |
|
37 | |||
@@ -218,6 +219,7 b' class EmptyChangeset(BaseChangeset):' | |||||
218 |
|
219 | |||
219 | revision = -1 |
|
220 | revision = -1 | |
220 | message = '' |
|
221 | message = '' | |
|
222 | author = '' | |||
221 |
|
223 | |||
222 | @LazyProperty |
|
224 | @LazyProperty | |
223 | def raw_id(self): |
|
225 | def raw_id(self): | |
@@ -362,3 +364,75 b' class OrderedDict(dict, DictMixin):' | |||||
362 |
|
364 | |||
363 | def __ne__(self, other): |
|
365 | def __ne__(self, other): | |
364 | return not self == other |
|
366 | return not self == other | |
|
367 | ||||
|
368 | ||||
|
369 | #=============================================================================== | |||
|
370 | # TEST FUNCTIONS | |||
|
371 | #=============================================================================== | |||
|
372 | def create_test_index(repo_location, full_index): | |||
|
373 | """Makes default test index | |||
|
374 | @param repo_location: | |||
|
375 | @param full_index: | |||
|
376 | """ | |||
|
377 | from pylons_app.lib.indexers.daemon import WhooshIndexingDaemon | |||
|
378 | from pylons_app.lib.pidlock import DaemonLock, LockHeld | |||
|
379 | from pylons_app.lib.indexers import IDX_LOCATION | |||
|
380 | import shutil | |||
|
381 | ||||
|
382 | if os.path.exists(IDX_LOCATION): | |||
|
383 | shutil.rmtree(IDX_LOCATION) | |||
|
384 | ||||
|
385 | try: | |||
|
386 | l = DaemonLock() | |||
|
387 | WhooshIndexingDaemon(repo_location=repo_location)\ | |||
|
388 | .run(full_index=full_index) | |||
|
389 | l.release() | |||
|
390 | except LockHeld: | |||
|
391 | pass | |||
|
392 | ||||
|
393 | def create_test_env(repos_test_path, config): | |||
|
394 | """Makes a fresh database and | |||
|
395 | install test repository into tmp dir | |||
|
396 | """ | |||
|
397 | from pylons_app.lib.db_manage import DbManage | |||
|
398 | import tarfile | |||
|
399 | import shutil | |||
|
400 | from os.path import dirname as dn, join as jn, abspath | |||
|
401 | ||||
|
402 | log = logging.getLogger('TestEnvCreator') | |||
|
403 | # create logger | |||
|
404 | log.setLevel(logging.DEBUG) | |||
|
405 | log.propagate = True | |||
|
406 | # create console handler and set level to debug | |||
|
407 | ch = logging.StreamHandler() | |||
|
408 | ch.setLevel(logging.DEBUG) | |||
|
409 | ||||
|
410 | # create formatter | |||
|
411 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |||
|
412 | ||||
|
413 | # add formatter to ch | |||
|
414 | ch.setFormatter(formatter) | |||
|
415 | ||||
|
416 | # add ch to logger | |||
|
417 | log.addHandler(ch) | |||
|
418 | ||||
|
419 | #PART ONE create db | |||
|
420 | log.debug('making test db') | |||
|
421 | dbname = config['sqlalchemy.db1.url'].split('/')[-1] | |||
|
422 | dbmanage = DbManage(log_sql=True, dbname=dbname, tests=True) | |||
|
423 | dbmanage.create_tables(override=True) | |||
|
424 | dbmanage.config_prompt(repos_test_path) | |||
|
425 | dbmanage.create_default_user() | |||
|
426 | dbmanage.admin_prompt() | |||
|
427 | dbmanage.create_permissions() | |||
|
428 | dbmanage.populate_default_permissions() | |||
|
429 | ||||
|
430 | #PART TWO make test repo | |||
|
431 | log.debug('making test vcs repo') | |||
|
432 | if os.path.isdir('/tmp/vcs_test'): | |||
|
433 | shutil.rmtree('/tmp/vcs_test') | |||
|
434 | ||||
|
435 | cur_dir = dn(dn(abspath(__file__))) | |||
|
436 | tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test.tar.gz")) | |||
|
437 | tar.extractall('/tmp') | |||
|
438 | tar.close() |
@@ -1,15 +1,8 b'' | |||||
1 | """The application's model objects""" |
|
1 | """The application's model objects""" | |
2 | import logging |
|
2 | import logging | |
3 | import sqlalchemy as sa |
|
|||
4 | from sqlalchemy import orm |
|
|||
5 | from pylons_app.model import meta |
|
3 | from pylons_app.model import meta | |
6 | from pylons_app.model.meta import Session |
|
|||
7 | log = logging.getLogger(__name__) |
|
4 | log = logging.getLogger(__name__) | |
8 |
|
5 | |||
9 | # Add these two imports: |
|
|||
10 | import datetime |
|
|||
11 | from sqlalchemy import schema, types |
|
|||
12 |
|
||||
13 | def init_model(engine): |
|
6 | def init_model(engine): | |
14 | """Call me before using any of the tables or classes in the model""" |
|
7 | """Call me before using any of the tables or classes in the model""" | |
15 | log.info("INITIALIZING DB MODELS") |
|
8 | log.info("INITIALIZING DB MODELS") |
@@ -26,7 +26,7 b' class HgAppUi(Base):' | |||||
26 |
|
26 | |||
27 | class User(Base): |
|
27 | class User(Base): | |
28 | __tablename__ = 'users' |
|
28 | __tablename__ = 'users' | |
29 | __table_args__ = (UniqueConstraint('username'), {'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) | |
@@ -120,6 +120,15 b' class UserToPerm(Base):' | |||||
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 | |||
124 |
|
132 | |||
|
133 | repository = relation('Repository') | |||
125 |
|
134 |
@@ -209,6 +209,36 b' class ValidPath(formencode.validators.Fa' | |||||
209 | raise formencode.Invalid(msg, value, state, |
|
209 | raise formencode.Invalid(msg, value, state, | |
210 | error_dict={'paths_root_path':msg}) |
|
210 | error_dict={'paths_root_path':msg}) | |
211 |
|
211 | |||
|
212 | def UniqSystemEmail(old_data): | |||
|
213 | class _UniqSystemEmail(formencode.validators.FancyValidator): | |||
|
214 | def to_python(self, value, state): | |||
|
215 | if old_data.get('email') != value: | |||
|
216 | sa = meta.Session | |||
|
217 | try: | |||
|
218 | user = sa.query(User).filter(User.email == value).scalar() | |||
|
219 | if user: | |||
|
220 | raise formencode.Invalid(_("That e-mail address is already taken") , | |||
|
221 | value, state) | |||
|
222 | finally: | |||
|
223 | meta.Session.remove() | |||
|
224 | ||||
|
225 | return value | |||
|
226 | ||||
|
227 | return _UniqSystemEmail | |||
|
228 | ||||
|
229 | class ValidSystemEmail(formencode.validators.FancyValidator): | |||
|
230 | def to_python(self, value, state): | |||
|
231 | sa = meta.Session | |||
|
232 | try: | |||
|
233 | user = sa.query(User).filter(User.email == value).scalar() | |||
|
234 | if user is None: | |||
|
235 | raise formencode.Invalid(_("That e-mail address doesn't exist.") , | |||
|
236 | value, state) | |||
|
237 | finally: | |||
|
238 | meta.Session.remove() | |||
|
239 | ||||
|
240 | return value | |||
|
241 | ||||
212 | #=============================================================================== |
|
242 | #=============================================================================== | |
213 | # FORMS |
|
243 | # FORMS | |
214 | #=============================================================================== |
|
244 | #=============================================================================== | |
@@ -250,12 +280,18 b' def UserForm(edit=False, old_data={}):' | |||||
250 | active = StringBoolean(if_missing=False) |
|
280 | active = StringBoolean(if_missing=False) | |
251 | name = UnicodeString(strip=True, min=3, not_empty=True) |
|
281 | name = UnicodeString(strip=True, min=3, not_empty=True) | |
252 | lastname = UnicodeString(strip=True, min=3, not_empty=True) |
|
282 | lastname = UnicodeString(strip=True, min=3, not_empty=True) | |
253 | email = Email(not_empty=True) |
|
283 | email = All(Email(not_empty=True), UniqSystemEmail(old_data)) | |
254 |
|
284 | |||
255 | return _UserForm |
|
285 | return _UserForm | |
256 |
|
286 | |||
257 | RegisterForm = UserForm |
|
287 | RegisterForm = UserForm | |
258 |
|
288 | |||
|
289 | def PasswordResetForm(): | |||
|
290 | class _PasswordResetForm(formencode.Schema): | |||
|
291 | allow_extra_fields = True | |||
|
292 | filter_extra_fields = True | |||
|
293 | email = All(ValidSystemEmail(), Email(not_empty=True)) | |||
|
294 | return _PasswordResetForm | |||
259 |
|
295 | |||
260 | def RepoForm(edit=False, old_data={}): |
|
296 | def RepoForm(edit=False, old_data={}): | |
261 | class _RepoForm(formencode.Schema): |
|
297 | class _RepoForm(formencode.Schema): |
@@ -43,16 +43,14 b' except ImportError:' | |||||
43 | raise Exception('Unable to import vcs') |
|
43 | raise Exception('Unable to import vcs') | |
44 |
|
44 | |||
45 | def _get_repos_cached_initial(app_globals, initial): |
|
45 | def _get_repos_cached_initial(app_globals, initial): | |
46 | """ |
|
46 | """return cached dict with repos | |
47 | return cached dict with repos |
|
|||
48 | """ |
|
47 | """ | |
49 | g = app_globals |
|
48 | g = app_globals | |
50 | return HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui, initial) |
|
49 | return HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui, initial) | |
51 |
|
50 | |||
52 | @cache_region('long_term', 'cached_repo_list') |
|
51 | @cache_region('long_term', 'cached_repo_list') | |
53 | def _get_repos_cached(): |
|
52 | def _get_repos_cached(): | |
54 | """ |
|
53 | """return cached dict with repos | |
55 | return cached dict with repos |
|
|||
56 | """ |
|
54 | """ | |
57 | log.info('getting all repositories list') |
|
55 | log.info('getting all repositories list') | |
58 | from pylons import app_globals as g |
|
56 | from pylons import app_globals as g | |
@@ -61,11 +59,12 b' def _get_repos_cached():' | |||||
61 | @cache_region('super_short_term', 'cached_repos_switcher_list') |
|
59 | @cache_region('super_short_term', 'cached_repos_switcher_list') | |
62 | def _get_repos_switcher_cached(cached_repo_list): |
|
60 | def _get_repos_switcher_cached(cached_repo_list): | |
63 | repos_lst = [] |
|
61 | repos_lst = [] | |
64 |
for repo in |
|
62 | for repo in [x for x in cached_repo_list.values()]: | |
65 |
if HasRepoPermissionAny('repository.write', 'repository.read', |
|
63 | if HasRepoPermissionAny('repository.write', 'repository.read', | |
66 | repos_lst.append(repo) |
|
64 | 'repository.admin')(repo.name.lower(), 'main page check'): | |
|
65 | repos_lst.append((repo.name, repo.dbrepo.private,)) | |||
67 |
|
66 | |||
68 | return repos_lst |
|
67 | return sorted(repos_lst, key=lambda k:k[0]) | |
69 |
|
68 | |||
70 | @cache_region('long_term', 'full_changelog') |
|
69 | @cache_region('long_term', 'full_changelog') | |
71 | def _full_changelog_cached(repo_name): |
|
70 | def _full_changelog_cached(repo_name): | |
@@ -73,14 +72,11 b' def _full_changelog_cached(repo_name):' | |||||
73 | return list(reversed(list(HgModel().get_repo(repo_name)))) |
|
72 | return list(reversed(list(HgModel().get_repo(repo_name)))) | |
74 |
|
73 | |||
75 | class HgModel(object): |
|
74 | class HgModel(object): | |
76 | """ |
|
75 | """Mercurial Model | |
77 | Mercurial Model |
|
|||
78 | """ |
|
76 | """ | |
79 |
|
77 | |||
80 | def __init__(self): |
|
78 | def __init__(self): | |
81 |
|
|
79 | pass | |
82 | Constructor |
|
|||
83 | """ |
|
|||
84 |
|
80 | |||
85 | @staticmethod |
|
81 | @staticmethod | |
86 | def repo_scan(repos_prefix, repos_path, baseui, initial=False): |
|
82 | def repo_scan(repos_prefix, repos_path, baseui, initial=False): | |
@@ -92,8 +88,7 b' class HgModel(object):' | |||||
92 | """ |
|
88 | """ | |
93 | sa = meta.Session() |
|
89 | sa = meta.Session() | |
94 | def check_repo_dir(path): |
|
90 | def check_repo_dir(path): | |
95 | """ |
|
91 | """Checks the repository | |
96 | Checks the repository |
|
|||
97 | :param path: |
|
92 | :param path: | |
98 | """ |
|
93 | """ | |
99 | repos_path = path.split('/') |
|
94 | repos_path = path.split('/') | |
@@ -102,7 +97,7 b' class HgModel(object):' | |||||
102 | if repos_path[0] != '/': |
|
97 | if repos_path[0] != '/': | |
103 | repos_path[0] = '/' |
|
98 | repos_path[0] = '/' | |
104 | if not os.path.isdir(os.path.join(*repos_path)): |
|
99 | if not os.path.isdir(os.path.join(*repos_path)): | |
105 |
raise RepositoryError('Not a valid repository in %s' % path |
|
100 | raise RepositoryError('Not a valid repository in %s' % path) | |
106 | if not repos_path.endswith('*'): |
|
101 | if not repos_path.endswith('*'): | |
107 | raise VCSError('You need to specify * or ** at the end of path ' |
|
102 | raise VCSError('You need to specify * or ** at the end of path ' | |
108 | 'for recursive scanning') |
|
103 | 'for recursive scanning') |
@@ -1,15 +1,58 b'' | |||||
1 | """SQLAlchemy Metadata and Session object""" |
|
1 | """SQLAlchemy Metadata and Session object""" | |
2 | from sqlalchemy.ext.declarative import declarative_base |
|
2 | from sqlalchemy.ext.declarative import declarative_base | |
3 | from sqlalchemy.orm import scoped_session, sessionmaker |
|
3 | from sqlalchemy.orm import scoped_session, sessionmaker | |
|
4 | from pylons_app.model import caching_query | |||
|
5 | from beaker import cache | |||
|
6 | import os | |||
|
7 | from os.path import join as jn, dirname as dn, abspath | |||
|
8 | import time | |||
|
9 | ||||
|
10 | # Beaker CacheManager. A home base for cache configurations. | |||
|
11 | cache_manager = cache.CacheManager() | |||
4 |
|
12 | |||
5 | __all__ = ['Base', 'Session'] |
|
13 | __all__ = ['Base', 'Session'] | |
6 | # |
|
14 | # | |
7 | # SQLAlchemy session manager. Updated by model.init_model() |
|
15 | # SQLAlchemy session manager. Updated by model.init_model() | |
8 | # |
|
16 | # | |
9 |
Session = scoped_session( |
|
17 | Session = scoped_session( | |
10 | # |
|
18 | sessionmaker( | |
|
19 | query_cls=caching_query.query_callable(cache_manager) | |||
|
20 | ) | |||
|
21 | ) | |||
11 |
|
22 | |||
12 | # The declarative Base |
|
23 | # The declarative Base | |
13 | Base = declarative_base() |
|
24 | Base = declarative_base() | |
14 | #For another db... |
|
25 | #For another db... | |
15 | #Base2 = declarative_base() |
|
26 | #Base2 = declarative_base() | |
|
27 | ||||
|
28 | #=============================================================================== | |||
|
29 | # CACHE OPTIONS | |||
|
30 | #=============================================================================== | |||
|
31 | cache_dir = jn(dn(dn(dn(abspath(__file__)))), 'data', 'cache') | |||
|
32 | if not os.path.isdir(cache_dir): | |||
|
33 | os.mkdir(cache_dir) | |||
|
34 | # set start_time to current time | |||
|
35 | # to re-cache everything | |||
|
36 | # upon application startup | |||
|
37 | start_time = time.time() | |||
|
38 | # configure the "sqlalchemy" cache region. | |||
|
39 | cache_manager.regions['sql_cache_short'] = { | |||
|
40 | 'type':'memory', | |||
|
41 | 'data_dir':cache_dir, | |||
|
42 | 'expire':10, | |||
|
43 | 'start_time':start_time | |||
|
44 | } | |||
|
45 | cache_manager.regions['sql_cache_med'] = { | |||
|
46 | 'type':'memory', | |||
|
47 | 'data_dir':cache_dir, | |||
|
48 | 'expire':360, | |||
|
49 | 'start_time':start_time | |||
|
50 | } | |||
|
51 | cache_manager.regions['sql_cache_long'] = { | |||
|
52 | 'type':'file', | |||
|
53 | 'data_dir':cache_dir, | |||
|
54 | 'expire':3600, | |||
|
55 | 'start_time':start_time | |||
|
56 | } | |||
|
57 | #to use cache use this in query | |||
|
58 | #.options(FromCache("sqlalchemy_cache_type", "cachekey")) |
@@ -2,7 +2,7 b'' | |||||
2 | # encoding: utf-8 |
|
2 | # encoding: utf-8 | |
3 | # Model for users |
|
3 | # Model for users | |
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 | |
@@ -23,10 +23,12 b' Created on April 9, 2010' | |||||
23 | Model for users |
|
23 | Model for users | |
24 | @author: marcink |
|
24 | @author: marcink | |
25 | """ |
|
25 | """ | |
26 |
|
26 | from pylons_app.lib import auth | ||
|
27 | from pylons.i18n.translation import _ | |||
|
28 | from pylons_app.lib.celerylib import tasks, run_task | |||
27 | from pylons_app.model.db import User |
|
29 | from pylons_app.model.db import User | |
28 | from pylons_app.model.meta import Session |
|
30 | from pylons_app.model.meta import Session | |
29 | from pylons.i18n.translation import _ |
|
31 | import traceback | |
30 | import logging |
|
32 | import logging | |
31 | log = logging.getLogger(__name__) |
|
33 | log = logging.getLogger(__name__) | |
32 |
|
34 | |||
@@ -54,8 +56,8 b' class UserModel(object):' | |||||
54 |
|
56 | |||
55 | self.sa.add(new_user) |
|
57 | self.sa.add(new_user) | |
56 | self.sa.commit() |
|
58 | self.sa.commit() | |
57 |
except |
|
59 | except: | |
58 | log.error(e) |
|
60 | log.error(traceback.format_exc()) | |
59 | self.sa.rollback() |
|
61 | self.sa.rollback() | |
60 | raise |
|
62 | raise | |
61 |
|
63 | |||
@@ -68,8 +70,8 b' class UserModel(object):' | |||||
68 |
|
70 | |||
69 | self.sa.add(new_user) |
|
71 | self.sa.add(new_user) | |
70 | self.sa.commit() |
|
72 | self.sa.commit() | |
71 |
except |
|
73 | except: | |
72 | log.error(e) |
|
74 | log.error(traceback.format_exc()) | |
73 | self.sa.rollback() |
|
75 | self.sa.rollback() | |
74 | raise |
|
76 | raise | |
75 |
|
77 | |||
@@ -88,8 +90,8 b' class UserModel(object):' | |||||
88 |
|
90 | |||
89 | self.sa.add(new_user) |
|
91 | self.sa.add(new_user) | |
90 | self.sa.commit() |
|
92 | self.sa.commit() | |
91 |
except |
|
93 | except: | |
92 | log.error(e) |
|
94 | log.error(traceback.format_exc()) | |
93 | self.sa.rollback() |
|
95 | self.sa.rollback() | |
94 | raise |
|
96 | raise | |
95 |
|
97 | |||
@@ -109,13 +111,12 b' class UserModel(object):' | |||||
109 |
|
111 | |||
110 | self.sa.add(new_user) |
|
112 | self.sa.add(new_user) | |
111 | self.sa.commit() |
|
113 | self.sa.commit() | |
112 |
except |
|
114 | except: | |
113 | log.error(e) |
|
115 | log.error(traceback.format_exc()) | |
114 | self.sa.rollback() |
|
116 | self.sa.rollback() | |
115 | raise |
|
117 | raise | |
116 |
|
118 | |||
117 | def delete(self, id): |
|
119 | def delete(self, id): | |
118 |
|
||||
119 | try: |
|
120 | try: | |
120 |
|
121 | |||
121 | user = self.sa.query(User).get(id) |
|
122 | user = self.sa.query(User).get(id) | |
@@ -125,7 +126,10 b' class UserModel(object):' | |||||
125 | " crucial for entire application")) |
|
126 | " crucial for entire application")) | |
126 | self.sa.delete(user) |
|
127 | self.sa.delete(user) | |
127 | self.sa.commit() |
|
128 | self.sa.commit() | |
128 |
except |
|
129 | except: | |
129 | log.error(e) |
|
130 | log.error(traceback.format_exc()) | |
130 | self.sa.rollback() |
|
131 | self.sa.rollback() | |
131 | raise |
|
132 | raise | |
|
133 | ||||
|
134 | def reset_password(self, data): | |||
|
135 | run_task(tasks.reset_user_password, data['email']) |
@@ -505,6 +505,33 b' div.options a:hover' | |||||
505 |
|
505 | |||
506 |
|
506 | |||
507 | /*ICONS*/ |
|
507 | /*ICONS*/ | |
|
508 | #header #header-inner #quick li ul li a.journal, | |||
|
509 | #header #header-inner #quick li ul li a.journal:hover | |||
|
510 | { | |||
|
511 | background:url("../images/icons/book.png") no-repeat scroll 4px 9px #FFFFFF; | |||
|
512 | margin:0; | |||
|
513 | padding:12px 9px 7px 24px; | |||
|
514 | width:167px; | |||
|
515 | ||||
|
516 | } | |||
|
517 | #header #header-inner #quick li ul li a.private_repo, | |||
|
518 | #header #header-inner #quick li ul li a.private_repo:hover | |||
|
519 | { | |||
|
520 | background:url("../images/icons/lock.png") no-repeat scroll 4px 9px #FFFFFF; | |||
|
521 | margin:0; | |||
|
522 | padding:12px 9px 7px 24px; | |||
|
523 | width:167px; | |||
|
524 | ||||
|
525 | } | |||
|
526 | #header #header-inner #quick li ul li a.public_repo, | |||
|
527 | #header #header-inner #quick li ul li a.public_repo:hover | |||
|
528 | { | |||
|
529 | background:url("../images/icons/lock_open.png") no-repeat scroll 4px 9px #FFFFFF; | |||
|
530 | margin:0; | |||
|
531 | padding:12px 9px 7px 24px; | |||
|
532 | width:167px; | |||
|
533 | ||||
|
534 | } | |||
508 |
|
535 | |||
509 | #header #header-inner #quick li ul li a.repos, |
|
536 | #header #header-inner #quick li ul li a.repos, | |
510 | #header #header-inner #quick li ul li a.repos:hover |
|
537 | #header #header-inner #quick li ul li a.repos:hover | |
@@ -2877,7 +2904,7 b' div.form div.fields div.buttons input' | |||||
2877 | #register div.form div.fields div.buttons |
|
2904 | #register div.form div.fields div.buttons | |
2878 | { |
|
2905 | { | |
2879 | margin: 0; |
|
2906 | margin: 0; | |
2880 |
padding: 10px 0 0 |
|
2907 | padding: 10px 0 0 114px; | |
2881 | clear: both; |
|
2908 | clear: both; | |
2882 | overflow: hidden; |
|
2909 | overflow: hidden; | |
2883 | border-top: 1px solid #DDDDDD; |
|
2910 | border-top: 1px solid #DDDDDD; |
@@ -11,8 +11,8 b'' | |||||
11 |
|
11 | |||
12 | %for cnt,l in enumerate(c.users_log): |
|
12 | %for cnt,l in enumerate(c.users_log): | |
13 | <tr class="parity${cnt%2}"> |
|
13 | <tr class="parity${cnt%2}"> | |
14 | <td>${l.user.username}</td> |
|
14 | <td>${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))}</td> | |
15 | <td>${l.repository}</td> |
|
15 | <td>${h.link_to(l.repository,h.url('summary_home',repo_name=l.repository))}</td> | |
16 | <td>${l.action}</td> |
|
16 | <td>${l.action}</td> | |
17 | <td>${l.action_date}</td> |
|
17 | <td>${l.action_date}</td> | |
18 | <td>${l.user_ip}</td> |
|
18 | <td>${l.user_ip}</td> |
@@ -29,7 +29,7 b'' | |||||
29 |
|
29 | |||
30 | <div class="field"> |
|
30 | <div class="field"> | |
31 | <div class="label"> |
|
31 | <div class="label"> | |
32 |
<label for="default_perm">${_(' |
|
32 | <label for="default_perm">${_('Repository permission')}:</label> | |
33 | </div> |
|
33 | </div> | |
34 | <div class="select"> |
|
34 | <div class="select"> | |
35 | ${h.select('default_perm','',c.perms_choices)} |
|
35 | ${h.select('default_perm','',c.perms_choices)} | |
@@ -51,7 +51,7 b'' | |||||
51 | </div> |
|
51 | </div> | |
52 | <div class="field"> |
|
52 | <div class="field"> | |
53 | <div class="label"> |
|
53 | <div class="label"> | |
54 |
<label for="default_create">${_(' |
|
54 | <label for="default_create">${_('Repository creation')}:</label> | |
55 | </div> |
|
55 | </div> | |
56 | <div class="select"> |
|
56 | <div class="select"> | |
57 | ${h.select('default_create','',c.create_choices)} |
|
57 | ${h.select('default_create','',c.create_choices)} |
@@ -48,6 +48,31 b'' | |||||
48 | </div> |
|
48 | </div> | |
49 | ${h.end_form()} |
|
49 | ${h.end_form()} | |
50 |
|
|
50 | ||
|
51 | <h3>${_('Whoosh indexing')}</h3> | |||
|
52 | ${h.form(url('admin_setting', setting_id='whoosh'),method='put')} | |||
|
53 | <div class="form"> | |||
|
54 | <!-- fields --> | |||
|
55 | ||||
|
56 | <div class="fields"> | |||
|
57 | <div class="field"> | |||
|
58 | <div class="label label-checkbox"> | |||
|
59 | <label for="destroy">${_('index build option')}:</label> | |||
|
60 | </div> | |||
|
61 | <div class="checkboxes"> | |||
|
62 | <div class="checkbox"> | |||
|
63 | ${h.checkbox('full_index',True)} | |||
|
64 | <label for="checkbox-1">${_('build from scratch')}</label> | |||
|
65 | </div> | |||
|
66 | </div> | |||
|
67 | </div> | |||
|
68 | ||||
|
69 | <div class="buttons"> | |||
|
70 | ${h.submit('reindex','reindex',class_="ui-button ui-widget ui-state-default ui-corner-all")} | |||
|
71 | </div> | |||
|
72 | </div> | |||
|
73 | </div> | |||
|
74 | ${h.end_form()} | |||
|
75 | ||||
51 | <h3>${_('Global application settings')}</h3> |
|
76 | <h3>${_('Global application settings')}</h3> | |
52 | ${h.form(url('admin_setting', setting_id='global'),method='put')} |
|
77 | ${h.form(url('admin_setting', setting_id='global'),method='put')} | |
53 | <div class="form"> |
|
78 | <div class="form"> |
@@ -97,8 +97,12 b'' | |||||
97 | <span>↓</span> |
|
97 | <span>↓</span> | |
98 | </a> |
|
98 | </a> | |
99 | <ul class="repo_switcher"> |
|
99 | <ul class="repo_switcher"> | |
100 | %for repo in c.repo_switcher_list: |
|
100 | %for repo,private in c.repo_switcher_list: | |
101 | <li>${h.link_to(repo,h.url('summary_home',repo_name=repo))}</li> |
|
101 | %if private: | |
|
102 | <li>${h.link_to(repo,h.url('summary_home',repo_name=repo),class_="private_repo")}</li> | |||
|
103 | %else: | |||
|
104 | <li>${h.link_to(repo,h.url('summary_home',repo_name=repo),class_="public_repo")}</li> | |||
|
105 | %endif | |||
102 | %endfor |
|
106 | %endfor | |
103 | </ul> |
|
107 | </ul> | |
104 | </li> |
|
108 | </li> | |
@@ -203,6 +207,7 b'' | |||||
203 | <span>${_('Admin')}</span> |
|
207 | <span>${_('Admin')}</span> | |
204 | </a> |
|
208 | </a> | |
205 | <ul> |
|
209 | <ul> | |
|
210 | <li>${h.link_to(_('journal'),h.url('admin_home'),class_='journal')}</li> | |||
206 | <li>${h.link_to(_('repositories'),h.url('repos'),class_='repos')}</li> |
|
211 | <li>${h.link_to(_('repositories'),h.url('repos'),class_='repos')}</li> | |
207 | <li>${h.link_to(_('users'),h.url('users'),class_='users')}</li> |
|
212 | <li>${h.link_to(_('users'),h.url('users'),class_='users')}</li> | |
208 | <li>${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li> |
|
213 | <li>${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li> |
@@ -23,18 +23,22 b'' | |||||
23 | </div> |
|
23 | </div> | |
24 | <div class="table"> |
|
24 | <div class="table"> | |
25 | <div id="files_data"> |
|
25 | <div id="files_data"> | |
26 |
<h |
|
26 | <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cur_rev,c.file.path)}</h3> | |
27 | <dl class="overview"> |
|
27 | <dl class="overview"> | |
28 | <dt>${_('Last revision')}</dt> |
|
28 | <dt>${_('Last revision')}</dt> | |
29 | <dd>${h.link_to("r%s:%s" % (c.file.last_changeset.revision,c.file.last_changeset._short), |
|
29 | <dd>${h.link_to("r%s:%s" % (c.file.last_changeset.revision,c.file.last_changeset._short), | |
30 | h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.last_changeset._short,f_path=c.f_path))} </dd> |
|
30 | h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.last_changeset._short,f_path=c.f_path))} </dd> | |
31 | <dt>${_('Size')}</dt> |
|
31 | <dt>${_('Size')}</dt> | |
32 | <dd>${h.format_byte_size(c.file.size,binary=True)}</dd> |
|
32 | <dd>${h.format_byte_size(c.file.size,binary=True)}</dd> | |
|
33 | <dt>${_('Mimetype')}</dt> | |||
|
34 | <dd>${c.file.mimetype}</dd> | |||
33 | <dt>${_('Options')}</dt> |
|
35 | <dt>${_('Options')}</dt> | |
34 | <dd>${h.link_to(_('show source'), |
|
36 | <dd>${h.link_to(_('show source'), | |
35 | h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} |
|
37 | h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |
|
38 | / ${h.link_to(_('show as raw'), | |||
|
39 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |||
36 | / ${h.link_to(_('download as raw'), |
|
40 | / ${h.link_to(_('download as raw'), | |
37 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} |
|
41 | h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |
38 | </dd> |
|
42 | </dd> | |
39 | </dl> |
|
43 | </dl> | |
40 | <div id="body" class="codeblock"> |
|
44 | <div id="body" class="codeblock"> | |
@@ -43,7 +47,12 b'' | |||||
43 | <div class="commit">"${c.file_msg}"</div> |
|
47 | <div class="commit">"${c.file_msg}"</div> | |
44 | </div> |
|
48 | </div> | |
45 | <div class="code-body"> |
|
49 | <div class="code-body"> | |
|
50 | % if c.file.size < c.file_size_limit: | |||
46 | ${h.pygmentize_annotation(c.file,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} |
|
51 | ${h.pygmentize_annotation(c.file,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} | |
|
52 | %else: | |||
|
53 | ${_('File is to big to display')} ${h.link_to(_('show as raw'), | |||
|
54 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |||
|
55 | %endif | |||
47 | </div> |
|
56 | </div> | |
48 | </div> |
|
57 | </div> | |
49 | </div> |
|
58 | </div> |
@@ -23,30 +23,37 b'' | |||||
23 | <tr> |
|
23 | <tr> | |
24 | <th>${_('Name')}</th> |
|
24 | <th>${_('Name')}</th> | |
25 | <th>${_('Size')}</th> |
|
25 | <th>${_('Size')}</th> | |
|
26 | <th>${_('Mimetype')}</th> | |||
26 | <th>${_('Revision')}</th> |
|
27 | <th>${_('Revision')}</th> | |
27 | <th>${_('Last modified')}</th> |
|
28 | <th>${_('Last modified')}</th> | |
28 | <th>${_('Last commiter')}</th> |
|
29 | <th>${_('Last commiter')}</th> | |
29 | </tr> |
|
30 | </tr> | |
30 | </thead> |
|
31 | </thead> | |
|
32 | ||||
|
33 | % if c.files_list.parent: | |||
31 |
|
|
34 | <tr class="parity0"> | |
32 |
|
|
35 | <td> | |
33 | % if c.files_list.parent: |
|
|||
34 |
|
|
36 | ${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.files_list.parent.path),class_="browser-dir")} | |
35 | %endif |
|
|||
36 |
|
|
37 | </td> | |
37 |
|
|
38 | <td></td> | |
38 |
|
|
39 | <td></td> | |
39 |
|
|
40 | <td></td> | |
40 |
|
|
41 | <td></td> | |
|
42 | <td></td> | |||
41 |
|
|
43 | </tr> | |
|
44 | %endif | |||
|
45 | ||||
42 | %for cnt,node in enumerate(c.files_list,1): |
|
46 | %for cnt,node in enumerate(c.files_list,1): | |
43 | <tr class="parity${cnt%2}"> |
|
47 | <tr class="parity${cnt%2}"> | |
44 | <td> |
|
48 | <td> | |
45 | ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=node.path),class_=file_class(node))} |
|
49 | ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=node.path),class_=file_class(node))} | |
46 | </td> |
|
50 | </td> | |
47 | <td> |
|
51 | <td> | |
|
52 | ${h.format_byte_size(node.size,binary=True)} | |||
|
53 | </td> | |||
|
54 | <td> | |||
48 |
|
|
55 | %if node.is_file(): | |
49 | ${h.format_byte_size(node.size,binary=True)} |
|
56 | ${node.mimetype} | |
50 |
|
|
57 | %endif | |
51 | </td> |
|
58 | </td> | |
52 | <td> |
|
59 | <td> |
@@ -6,11 +6,15 b'' | |||||
6 | </dd> |
|
6 | </dd> | |
7 | <dt>${_('Size')}</dt> |
|
7 | <dt>${_('Size')}</dt> | |
8 | <dd>${h.format_byte_size(c.files_list.size,binary=True)}</dd> |
|
8 | <dd>${h.format_byte_size(c.files_list.size,binary=True)}</dd> | |
|
9 | <dt>${_('Mimetype')}</dt> | |||
|
10 | <dd>${c.files_list.mimetype}</dd> | |||
9 | <dt>${_('Options')}</dt> |
|
11 | <dt>${_('Options')}</dt> | |
10 | <dd>${h.link_to(_('show annotation'), |
|
12 | <dd>${h.link_to(_('show annotation'), | |
11 |
h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} |
|
13 | h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |
|
14 | / ${h.link_to(_('show as raw'), | |||
|
15 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |||
12 | / ${h.link_to(_('download as raw'), |
|
16 | / ${h.link_to(_('download as raw'), | |
13 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} |
|
17 | h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |
14 | </dd> |
|
18 | </dd> | |
15 | <dt>${_('History')}</dt> |
|
19 | <dt>${_('History')}</dt> | |
16 | <dd> |
|
20 | <dd> | |
@@ -32,7 +36,12 b'' | |||||
32 | <div class="commit">"${c.files_list.last_changeset.message}"</div> |
|
36 | <div class="commit">"${c.files_list.last_changeset.message}"</div> | |
33 | </div> |
|
37 | </div> | |
34 | <div class="code-body"> |
|
38 | <div class="code-body"> | |
|
39 | % if c.files_list.size < c.file_size_limit: | |||
35 | ${h.pygmentize(c.files_list,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} |
|
40 | ${h.pygmentize(c.files_list,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} | |
|
41 | %else: | |||
|
42 | ${_('File is to big to display')} ${h.link_to(_('show as raw'), | |||
|
43 | h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} | |||
|
44 | %endif | |||
36 | </div> |
|
45 | </div> | |
37 | </div> |
|
46 | </div> | |
38 |
|
47 |
@@ -60,7 +60,7 b'' | |||||
60 | <!-- end fields --> |
|
60 | <!-- end fields --> | |
61 | <!-- links --> |
|
61 | <!-- links --> | |
62 | <div class="links"> |
|
62 | <div class="links"> | |
63 |
${h.link_to(_('Forgot your password ?'),h.url(' |
|
63 | ${h.link_to(_('Forgot your password ?'),h.url('reset_password'))} | |
64 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): |
|
64 | %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')(): | |
65 | / |
|
65 | / | |
66 | ${h.link_to(_("Don't have an account ?"),h.url('register'))} |
|
66 | ${h.link_to(_("Don't have an account ?"),h.url('register'))} |
@@ -46,7 +46,7 b'' | |||||
46 | h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}</div> |
|
46 | h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}</div> | |
47 | </div> |
|
47 | </div> | |
48 | <div class="code-body"> |
|
48 | <div class="code-body"> | |
49 | <pre>${h.literal(sr['content_short'])}</pre> |
|
49 | <pre>${h.literal(sr['content_short_hl'])}</pre> | |
50 | </div> |
|
50 | </div> | |
51 | </div> |
|
51 | </div> | |
52 | </div> |
|
52 | </div> | |
@@ -61,9 +61,11 b'' | |||||
61 |
|
61 | |||
62 | %endif |
|
62 | %endif | |
63 | %endfor |
|
63 | %endfor | |
64 |
|
64 | %if c.cur_query: | ||
65 |
|
65 | <div class="pagination-wh pagination-left"> | ||
66 |
|
66 | ${c.formated_results.pager('$link_previous ~2~ $link_next')} | ||
|
67 | </div> | |||
|
68 | %endif | |||
67 | </div> |
|
69 | </div> | |
68 |
|
70 | |||
69 | </%def> |
|
71 | </%def> |
@@ -13,7 +13,7 b'' | |||||
13 | </tr> |
|
13 | </tr> | |
14 | %for cnt,cs in enumerate(c.repo_changesets): |
|
14 | %for cnt,cs in enumerate(c.repo_changesets): | |
15 | <tr class="parity${cnt%2}"> |
|
15 | <tr class="parity${cnt%2}"> | |
16 | <td>${h.age(cs._ctx.date())}</td> |
|
16 | <td>${h.age(cs._ctx.date())} - ${h.rfc822date_notz(cs._ctx.date())} </td> | |
17 | <td title="${cs.author}">${h.person(cs.author)}</td> |
|
17 | <td title="${cs.author}">${h.person(cs.author)}</td> | |
18 | <td>r${cs.revision}:${cs.raw_id}</td> |
|
18 | <td>r${cs.revision}:${cs.raw_id}</td> | |
19 | <td> |
|
19 | <td> |
@@ -77,6 +77,8 b' E.onDOMReady(function(e){' | |||||
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} | |||
|
81 | ||||
80 | </div> |
|
82 | </div> | |
81 | </div> |
|
83 | </div> | |
82 |
|
84 | |||
@@ -121,64 +123,114 b' E.onDOMReady(function(e){' | |||||
121 | <div class="box box-right" style="min-height:455px"> |
|
123 | <div class="box box-right" style="min-height:455px"> | |
122 | <!-- box / title --> |
|
124 | <!-- box / title --> | |
123 | <div class="title"> |
|
125 | <div class="title"> | |
124 |
<h5>${_(' |
|
126 | <h5>${_('Commit activity by day / author')}</h5> | |
125 | </div> |
|
127 | </div> | |
126 |
|
128 | |||
127 | <div class="table"> |
|
129 | <div class="table"> | |
128 | <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> | |
129 | <div id="legend_data"> |
|
131 | <div style="clear: both;height: 10px"></div> | |
|
132 | <div id="overview" style="width:560px;height:100px;float:left"></div> | |||
|
133 | ||||
|
134 | <div id="legend_data" style="clear:both;margin-top:10px;"> | |||
130 | <div id="legend_container"></div> |
|
135 | <div id="legend_container"></div> | |
131 | <div id="legend_choices"> |
|
136 | <div id="legend_choices"> | |
132 | <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> | |
133 | </div> |
|
138 | </div> | |
134 | </div> |
|
139 | </div> | |
135 | <script type="text/javascript"> |
|
140 | <script type="text/javascript"> | |
136 |
|
141 | /** | ||
137 | (function () { |
|
142 | * Plots summary graph | |
138 | var datasets = {${c.commit_data|n}}; |
|
143 | * | |
139 | var i = 0; |
|
144 | * @class SummaryPlot | |
|
145 | * @param {from} initial from for detailed graph | |||
|
146 | * @param {to} initial to for detailed graph | |||
|
147 | * @param {dataset} | |||
|
148 | * @param {overview_dataset} | |||
|
149 | */ | |||
|
150 | function SummaryPlot(from,to,dataset,overview_dataset) { | |||
|
151 | var initial_ranges = { | |||
|
152 | "xaxis":{ | |||
|
153 | "from":from, | |||
|
154 | "to":to, | |||
|
155 | }, | |||
|
156 | }; | |||
|
157 | var dataset = dataset; | |||
|
158 | var overview_dataset = [overview_dataset]; | |||
140 | var choiceContainer = YAHOO.util.Dom.get("legend_choices"); |
|
159 | var choiceContainer = YAHOO.util.Dom.get("legend_choices"); | |
141 | var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables"); |
|
160 | var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables"); | |
142 | for(var key in datasets) { |
|
161 | var plotContainer = YAHOO.util.Dom.get('commit_history'); | |
143 | datasets[key].color = i; |
|
162 | var overviewContainer = YAHOO.util.Dom.get('overview'); | |
144 |
|
|
163 | ||
145 | choiceContainerTable.innerHTML += '<tr><td>'+ |
|
164 | var plot_options = { | |
146 | '<input type="checkbox" name="' + key +'" checked="checked" />' |
|
165 | bars: {show:true,align:'center',lineWidth:4}, | |
147 | +datasets[key].label+ |
|
166 | legend: {show:true, container:"legend_container"}, | |
148 | '</td></tr>'; |
|
167 | points: {show:true,radius:0,fill:false}, | |
|
168 | yaxis: {tickDecimals:0,}, | |||
|
169 | xaxis: { | |||
|
170 | mode: "time", | |||
|
171 | timeformat: "%d/%m", | |||
|
172 | min:from, | |||
|
173 | max:to, | |||
|
174 | }, | |||
|
175 | grid: { | |||
|
176 | hoverable: true, | |||
|
177 | clickable: true, | |||
|
178 | autoHighlight:true, | |||
|
179 | color: "#999" | |||
|
180 | }, | |||
|
181 | //selection: {mode: "x"} | |||
|
182 | }; | |||
|
183 | var overview_options = { | |||
|
184 | legend:{show:false}, | |||
|
185 | bars: {show:true,barWidth: 2,}, | |||
|
186 | shadowSize: 0, | |||
|
187 | xaxis: {mode: "time", timeformat: "%d/%m/%y",}, | |||
|
188 | yaxis: {ticks: 3, min: 0,}, | |||
|
189 | grid: {color: "#999",}, | |||
|
190 | selection: {mode: "x"} | |||
149 |
|
|
191 | }; | |
150 |
|
192 | |||
151 |
|
193 | /** | ||
152 | function plotAccordingToChoices() { |
|
194 | *get dummy data needed in few places | |
153 | var data = []; |
|
195 | */ | |
154 |
|
196 | function getDummyData(label){ | ||
155 | var inputs = choiceContainer.getElementsByTagName("input"); |
|
197 | return {"label":label, | |
156 | for(var i=0; i<inputs.length; i++) { |
|
198 | "data":[{"time":0, | |
157 | var key = inputs[i].name; |
|
199 | "commits":0, | |
158 | if (key && datasets[key]){ |
|
200 | "added":0, | |
159 | if(!inputs[i].checked){ |
|
201 | "changed":0, | |
160 | data.push({label:key,data:[[0,1],]}); |
|
202 | "removed":0, | |
|
203 | }], | |||
|
204 | "schema":["commits"], | |||
|
205 | "color":'#ffffff', | |||
161 |
|
|
206 | } | |
162 | else{ |
|
|||
163 | data.push(datasets[key]); |
|
|||
164 | } |
|
207 | } | |
165 |
|
208 | |||
|
209 | /** | |||
|
210 | * generate checkboxes accordindly to data | |||
|
211 | * @param keys | |||
|
212 | * @returns | |||
|
213 | */ | |||
|
214 | function generateCheckboxes(data) { | |||
|
215 | //append checkboxes | |||
|
216 | var i = 0; | |||
|
217 | choiceContainerTable.innerHTML = ''; | |||
|
218 | for(var pos in data) { | |||
|
219 | ||||
|
220 | data[pos].color = i; | |||
|
221 | i++; | |||
|
222 | if(data[pos].label != ''){ | |||
|
223 | choiceContainerTable.innerHTML += '<tr><td>'+ | |||
|
224 | '<input type="checkbox" name="' + data[pos].label +'" checked="checked" />' | |||
|
225 | +data[pos].label+ | |||
|
226 | '</td></tr>'; | |||
|
227 | } | |||
|
228 | } | |||
166 |
|
|
229 | } | |
167 |
|
|
230 | ||
168 | }; |
|
231 | /** | |
169 |
|
232 | * ToolTip show | ||
170 | if (data.length > 0){ |
|
233 | */ | |
171 |
|
||||
172 | var plot = YAHOO.widget.Flot("commit_history", data, |
|
|||
173 | { bars: { show: true, align:'center',lineWidth:4 }, |
|
|||
174 | points: { show: true, radius:0,fill:true }, |
|
|||
175 | legend:{show:true, container:"legend_container"}, |
|
|||
176 | selection: { mode: "xy" }, |
|
|||
177 | yaxis: {tickDecimals:0}, |
|
|||
178 | xaxis: { mode: "time", timeformat: "%d",tickSize:[1, "day"],min:${c.ts_min},max:${c.ts_max} }, |
|
|||
179 | grid: { hoverable: true, clickable: true,autoHighlight:true }, |
|
|||
180 | }); |
|
|||
181 |
|
||||
182 |
|
|
234 | function showTooltip(x, y, contents) { | |
183 |
|
|
235 | var div=document.getElementById('tooltip'); | |
184 |
|
|
236 | if(!div) { | |
@@ -199,8 +251,143 b' E.onDOMReady(function(e){' | |||||
199 |
|
|
251 | anim.animate(); | |
200 |
|
|
252 | } | |
201 |
|
253 | |||
|
254 | /** | |||
|
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 | |||
|
257 | Additionally it will only display users that are selected by the checkbox | |||
|
258 | */ | |||
|
259 | function getDataAccordingToRanges(ranges) { | |||
|
260 | ||||
|
261 | var data = []; | |||
|
262 | var keys = []; | |||
|
263 | for(var key in dataset){ | |||
|
264 | var push = false; | |||
|
265 | //method1 slow !! | |||
|
266 | ///* | |||
|
267 | for(var ds in dataset[key].data){ | |||
|
268 | commit_data = dataset[key].data[ds]; | |||
|
269 | //console.log(key); | |||
|
270 | //console.log(new Date(commit_data.time*1000)); | |||
|
271 | //console.log(new Date(ranges.xaxis.from*1000)); | |||
|
272 | //console.log(new Date(ranges.xaxis.to*1000)); | |||
|
273 | if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){ | |||
|
274 | push = true; | |||
|
275 | break; | |||
|
276 | } | |||
|
277 | } | |||
|
278 | //*/ | |||
|
279 | /*//method2 sorted commit data !!! | |||
|
280 | var first_commit = dataset[key].data[0].time; | |||
|
281 | var last_commit = dataset[key].data[dataset[key].data.length-1].time; | |||
|
282 | ||||
|
283 | console.log(first_commit); | |||
|
284 | console.log(last_commit); | |||
|
285 | ||||
|
286 | if (first_commit >= ranges.xaxis.from && last_commit <= ranges.xaxis.to){ | |||
|
287 | push = true; | |||
|
288 | } | |||
|
289 | */ | |||
|
290 | if(push){ | |||
|
291 | data.push(dataset[key]); | |||
|
292 | } | |||
|
293 | } | |||
|
294 | if(data.length >= 1){ | |||
|
295 | return data; | |||
|
296 | } | |||
|
297 | else{ | |||
|
298 | //just return dummy data for graph to plot itself | |||
|
299 | return [getDummyData('')]; | |||
|
300 | } | |||
|
301 | ||||
|
302 | } | |||
|
303 | ||||
|
304 | /** | |||
|
305 | * redraw using new checkbox data | |||
|
306 | */ | |||
|
307 | function plotchoiced(e,args){ | |||
|
308 | var cur_data = args[0]; | |||
|
309 | var cur_ranges = args[1]; | |||
|
310 | ||||
|
311 | var new_data = []; | |||
|
312 | var inputs = choiceContainer.getElementsByTagName("input"); | |||
|
313 | ||||
|
314 | //show only checked labels | |||
|
315 | for(var i=0; i<inputs.length; i++) { | |||
|
316 | var checkbox_key = inputs[i].name; | |||
|
317 | ||||
|
318 | if(inputs[i].checked){ | |||
|
319 | for(var d in cur_data){ | |||
|
320 | if(cur_data[d].label == checkbox_key){ | |||
|
321 | new_data.push(cur_data[d]); | |||
|
322 | } | |||
|
323 | } | |||
|
324 | } | |||
|
325 | else{ | |||
|
326 | //push dummy data to not hide the label | |||
|
327 | new_data.push(getDummyData(checkbox_key)); | |||
|
328 | } | |||
|
329 | } | |||
|
330 | ||||
|
331 | var new_options = YAHOO.lang.merge(plot_options, { | |||
|
332 | xaxis: { | |||
|
333 | min: cur_ranges.xaxis.from, | |||
|
334 | max: cur_ranges.xaxis.to, | |||
|
335 | mode:"time", | |||
|
336 | timeformat: "%d/%m", | |||
|
337 | } | |||
|
338 | }); | |||
|
339 | if (!new_data){ | |||
|
340 | new_data = [[0,1]]; | |||
|
341 | } | |||
|
342 | // do the zooming | |||
|
343 | plot = YAHOO.widget.Flot(plotContainer, new_data, new_options); | |||
|
344 | ||||
|
345 | plot.subscribe("plotselected", plotselected); | |||
|
346 | ||||
|
347 | //resubscribe plothover | |||
|
348 | plot.subscribe("plothover", plothover); | |||
|
349 | ||||
|
350 | // don't fire event on the overview to prevent eternal loop | |||
|
351 | overview.setSelection(cur_ranges, true); | |||
|
352 | ||||
|
353 | } | |||
|
354 | ||||
|
355 | /** | |||
|
356 | * plot only selected items from overview | |||
|
357 | * @param ranges | |||
|
358 | * @returns | |||
|
359 | */ | |||
|
360 | function plotselected(ranges,cur_data) { | |||
|
361 | //updates the data for new plot | |||
|
362 | data = getDataAccordingToRanges(ranges); | |||
|
363 | generateCheckboxes(data); | |||
|
364 | ||||
|
365 | var new_options = YAHOO.lang.merge(plot_options, { | |||
|
366 | xaxis: { | |||
|
367 | min: ranges.xaxis.from, | |||
|
368 | max: ranges.xaxis.to, | |||
|
369 | mode:"time", | |||
|
370 | timeformat: "%d/%m", | |||
|
371 | } | |||
|
372 | }); | |||
|
373 | // do the zooming | |||
|
374 | plot = YAHOO.widget.Flot(plotContainer, data, new_options); | |||
|
375 | ||||
|
376 | plot.subscribe("plotselected", plotselected); | |||
|
377 | ||||
|
378 | //resubscribe plothover | |||
|
379 | plot.subscribe("plothover", plothover); | |||
|
380 | ||||
|
381 | // don't fire event on the overview to prevent eternal loop | |||
|
382 | overview.setSelection(ranges, true); | |||
|
383 | ||||
|
384 | //resubscribe choiced | |||
|
385 | YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]); | |||
|
386 | } | |||
|
387 | ||||
202 |
|
|
388 | var previousPoint = null; | |
203 | plot.subscribe("plothover", function (o) { |
|
389 | ||
|
390 | function plothover(o) { | |||
204 |
|
|
391 | var pos = o.pos; | |
205 |
|
|
392 | var item = o.item; | |
206 |
|
|
393 | ||
@@ -221,10 +408,10 b' E.onDOMReady(function(e){' | |||||
221 |
|
|
408 | item.series.label = 'commits'; | |
222 |
|
|
409 | } | |
223 |
|
|
410 | var d = new Date(x*1000); | |
224 | var fd = d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate(); |
|
411 | var fd = d.toDateString() | |
225 |
|
|
412 | var nr_commits = parseInt(y); | |
226 |
|
|
413 | ||
227 |
|
|
414 | var cur_data = dataset[item.series.label].data[item.dataIndex]; | |
228 |
|
|
415 | var added = cur_data.added; | |
229 |
|
|
416 | var changed = cur_data.changed; | |
230 |
|
|
417 | var removed = cur_data.removed; | |
@@ -256,15 +443,35 b' E.onDOMReady(function(e){' | |||||
256 |
|
|
443 | } | |
257 |
|
|
444 | previousPoint = null; | |
258 |
|
|
445 | } | |
259 | }); |
|
|||
260 |
|
||||
261 | } |
|
|||
262 | } |
|
446 | } | |
263 |
|
447 | |||
264 | YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotAccordingToChoices); |
|
448 | /** | |
|
449 | * MAIN EXECUTION | |||
|
450 | */ | |||
|
451 | ||||
|
452 | var data = getDataAccordingToRanges(initial_ranges); | |||
|
453 | generateCheckboxes(data); | |||
|
454 | ||||
|
455 | //main plot | |||
|
456 | var plot = YAHOO.widget.Flot(plotContainer,data,plot_options); | |||
|
457 | ||||
|
458 | //overview | |||
|
459 | var overview = YAHOO.widget.Flot(overviewContainer, overview_dataset, overview_options); | |||
265 |
|
460 | |||
266 | plotAccordingToChoices(); |
|
461 | //show initial selection on overview | |
267 | })(); |
|
462 | overview.setSelection(initial_ranges); | |
|
463 | ||||
|
464 | plot.subscribe("plotselected", plotselected); | |||
|
465 | ||||
|
466 | overview.subscribe("plotselected", function (ranges) { | |||
|
467 | plot.setSelection(ranges); | |||
|
468 | }); | |||
|
469 | ||||
|
470 | plot.subscribe("plothover", plothover); | |||
|
471 | ||||
|
472 | YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]); | |||
|
473 | } | |||
|
474 | SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n}); | |||
268 |
|
|
475 | </script> | |
269 |
|
476 | |||
270 | </div> |
|
477 | </div> |
@@ -16,12 +16,18 b' from routes.util import URLGenerator' | |||||
16 | from webtest import TestApp |
|
16 | from webtest import TestApp | |
17 | import os |
|
17 | import os | |
18 | from pylons_app.model import meta |
|
18 | from pylons_app.model import meta | |
|
19 | import logging | |||
|
20 | ||||
|
21 | ||||
|
22 | log = logging.getLogger(__name__) | |||
|
23 | ||||
19 | import pylons.test |
|
24 | import pylons.test | |
20 |
|
25 | |||
21 | __all__ = ['environ', 'url', 'TestController'] |
|
26 | __all__ = ['environ', 'url', 'TestController'] | |
22 |
|
27 | |||
23 | # Invoke websetup with the current config file |
|
28 | # Invoke websetup with the current config file | |
24 |
SetupCommand('setup-app').run([ |
|
29 | #SetupCommand('setup-app').run([config_file]) | |
|
30 | ||||
25 |
|
31 | |||
26 | environ = {} |
|
32 | environ = {} | |
27 |
|
33 | |||
@@ -33,13 +39,13 b' class TestController(TestCase):' | |||||
33 | self.app = TestApp(wsgiapp) |
|
39 | self.app = TestApp(wsgiapp) | |
34 | url._push_object(URLGenerator(config['routes.map'], environ)) |
|
40 | url._push_object(URLGenerator(config['routes.map'], environ)) | |
35 | self.sa = meta.Session |
|
41 | self.sa = meta.Session | |
|
42 | ||||
36 | TestCase.__init__(self, *args, **kwargs) |
|
43 | TestCase.__init__(self, *args, **kwargs) | |
37 |
|
44 | |||
38 |
|
45 | def log_user(self, username='test_admin', password='test'): | ||
39 | def log_user(self): |
|
|||
40 | response = self.app.post(url(controller='login', action='index'), |
|
46 | response = self.app.post(url(controller='login', action='index'), | |
41 |
{'username': |
|
47 | {'username':username, | |
42 |
'password': |
|
48 | 'password':password}) | |
43 | assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status |
|
49 | assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status | |
44 | assert response.session['hg_app_user'].username == 'test_admin', 'wrong logged in user' |
|
50 | assert response.session['hg_app_user'].username == 'test_admin', 'wrong logged in user' | |
45 |
return response.follow() |
|
51 | return response.follow() |
@@ -3,5 +3,7 b' from pylons_app.tests import *' | |||||
3 | class TestAdminController(TestController): |
|
3 | class TestAdminController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='admin/admin', action='index')) |
|
7 | response = self.app.get(url(controller='admin/admin', action='index')) | |
|
8 | assert 'Admin dashboard - journal' in response.body,'No proper title in dashboard' | |||
7 | # Test response... |
|
9 | # Test response... |
@@ -1,4 +1,5 b'' | |||||
1 | from pylons_app.tests import * |
|
1 | from pylons_app.tests import * | |
|
2 | from pylons_app.model.db import User | |||
2 |
|
3 | |||
3 | class TestSettingsController(TestController): |
|
4 | class TestSettingsController(TestController): | |
4 |
|
5 | |||
@@ -41,3 +42,75 b' class TestSettingsController(TestControl' | |||||
41 |
|
42 | |||
42 | def test_edit_as_xml(self): |
|
43 | def test_edit_as_xml(self): | |
43 | response = self.app.get(url('formatted_admin_edit_setting', setting_id=1, format='xml')) |
|
44 | response = self.app.get(url('formatted_admin_edit_setting', setting_id=1, format='xml')) | |
|
45 | ||||
|
46 | def test_my_account(self): | |||
|
47 | self.log_user() | |||
|
48 | response = self.app.get(url('admin_settings_my_account')) | |||
|
49 | print response | |||
|
50 | assert 'value="test_admin' in response.body | |||
|
51 | ||||
|
52 | ||||
|
53 | ||||
|
54 | def test_my_account_update(self): | |||
|
55 | self.log_user() | |||
|
56 | new_email = 'new@mail.pl' | |||
|
57 | response = self.app.post(url('admin_settings_my_account_update'), params=dict( | |||
|
58 | _method='put', | |||
|
59 | username='test_admin', | |||
|
60 | new_password='test', | |||
|
61 | password='', | |||
|
62 | name='NewName', | |||
|
63 | lastname='NewLastname', | |||
|
64 | email=new_email,)) | |||
|
65 | response.follow() | |||
|
66 | print response | |||
|
67 | ||||
|
68 | print 'x' * 100 | |||
|
69 | print response.session | |||
|
70 | assert 'Your account was updated succesfully' in response.session['flash'][0][1], 'no flash message about success of change' | |||
|
71 | user = self.sa.query(User).filter(User.username == 'test_admin').one() | |||
|
72 | assert user.email == new_email , 'incorrect user email after update got %s vs %s' % (user.email, new_email) | |||
|
73 | ||||
|
74 | def test_my_account_update_own_email_ok(self): | |||
|
75 | self.log_user() | |||
|
76 | ||||
|
77 | new_email = 'new@mail.pl' | |||
|
78 | response = self.app.post(url('admin_settings_my_account_update'), params=dict( | |||
|
79 | _method='put', | |||
|
80 | username='test_admin', | |||
|
81 | new_password='test', | |||
|
82 | name='NewName', | |||
|
83 | lastname='NewLastname', | |||
|
84 | email=new_email,)) | |||
|
85 | print response | |||
|
86 | ||||
|
87 | def test_my_account_update_err_email_exists(self): | |||
|
88 | self.log_user() | |||
|
89 | ||||
|
90 | new_email = 'test_regular@mail.com'#already exisitn email | |||
|
91 | response = self.app.post(url('admin_settings_my_account_update'), params=dict( | |||
|
92 | _method='put', | |||
|
93 | username='test_admin', | |||
|
94 | new_password='test', | |||
|
95 | name='NewName', | |||
|
96 | lastname='NewLastname', | |||
|
97 | email=new_email,)) | |||
|
98 | print response | |||
|
99 | ||||
|
100 | assert 'That e-mail address is already taken' in response.body, 'Missing error message about existing email' | |||
|
101 | ||||
|
102 | ||||
|
103 | def test_my_account_update_err(self): | |||
|
104 | self.log_user() | |||
|
105 | ||||
|
106 | new_email = 'newmail.pl' | |||
|
107 | response = self.app.post(url('admin_settings_my_account_update'), params=dict( | |||
|
108 | _method='put', | |||
|
109 | username='test_regular2', | |||
|
110 | new_password='test', | |||
|
111 | name='NewName', | |||
|
112 | lastname='NewLastname', | |||
|
113 | email=new_email,)) | |||
|
114 | print response | |||
|
115 | assert 'An email address must contain a single @' in response.body, 'Missing error message about wrong email' | |||
|
116 | assert 'This username already exists' in response.body, 'Missing error message about existing user' |
@@ -3,5 +3,6 b' from pylons_app.tests import *' | |||||
3 | class TestBranchesController(TestController): |
|
3 | class TestBranchesController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='branches', action='index',repo_name='vcs_test')) |
|
7 | response = self.app.get(url(controller='branches', action='index',repo_name='vcs_test')) | |
7 | # Test response... |
|
8 | # Test response... |
@@ -3,5 +3,6 b' from pylons_app.tests import *' | |||||
3 | class TestChangelogController(TestController): |
|
3 | class TestChangelogController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='changelog', action='index',repo_name='vcs_test')) |
|
7 | response = self.app.get(url(controller='changelog', action='index',repo_name='vcs_test')) | |
7 | # Test response... |
|
8 | # Test response... |
@@ -3,11 +3,13 b' from pylons_app.tests import *' | |||||
3 | class TestFeedController(TestController): |
|
3 | class TestFeedController(TestController): | |
4 |
|
4 | |||
5 | def test_rss(self): |
|
5 | def test_rss(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='feed', action='rss', |
|
7 | response = self.app.get(url(controller='feed', action='rss', | |
7 | repo_name='vcs_test')) |
|
8 | repo_name='vcs_test')) | |
8 | # Test response... |
|
9 | # Test response... | |
9 |
|
10 | |||
10 | def test_atom(self): |
|
11 | def test_atom(self): | |
|
12 | self.log_user() | |||
11 | response = self.app.get(url(controller='feed', action='atom', |
|
13 | response = self.app.get(url(controller='feed', action='atom', | |
12 | repo_name='vcs_test')) |
|
14 | repo_name='vcs_test')) | |
13 | # Test response... No newline at end of file |
|
15 | # Test response... |
@@ -3,6 +3,7 b' from pylons_app.tests import *' | |||||
3 | class TestFilesController(TestController): |
|
3 | class TestFilesController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='files', action='index', |
|
7 | response = self.app.get(url(controller='files', action='index', | |
7 | repo_name='vcs_test', |
|
8 | repo_name='vcs_test', | |
8 | revision='tip', |
|
9 | revision='tip', |
@@ -82,9 +82,9 b' class TestLoginController(TestController' | |||||
82 |
|
82 | |||
83 |
|
83 | |||
84 | def test_register_ok(self): |
|
84 | def test_register_ok(self): | |
85 |
username = 'test_regular |
|
85 | username = 'test_regular4' | |
86 | password = 'qweqwe' |
|
86 | password = 'qweqwe' | |
87 |
email = ' |
|
87 | email = 'marcin@test.com' | |
88 | name = 'testname' |
|
88 | name = 'testname' | |
89 | lastname = 'testlastname' |
|
89 | lastname = 'testlastname' | |
90 |
|
90 | |||
@@ -94,10 +94,11 b' class TestLoginController(TestController' | |||||
94 | 'email':email, |
|
94 | 'email':email, | |
95 | 'name':name, |
|
95 | 'name':name, | |
96 | 'lastname':lastname}) |
|
96 | 'lastname':lastname}) | |
97 |
|
97 | print response.body | ||
98 | assert response.status == '302 Found', 'Wrong response from register page got %s' % response.status |
|
98 | assert response.status == '302 Found', 'Wrong response from register page got %s' % response.status | |
|
99 | assert 'You have successfully registered into hg-app' in response.session['flash'][0], 'No flash message about user registration' | |||
99 |
|
100 | |||
100 |
ret = self.sa.query(User).filter(User.username == 'test_regular |
|
101 | ret = self.sa.query(User).filter(User.username == 'test_regular4').one() | |
101 | assert ret.username == username , 'field mismatch %s %s' % (ret.username, username) |
|
102 | assert ret.username == username , 'field mismatch %s %s' % (ret.username, username) | |
102 | assert check_password(password,ret.password) == True , 'password mismatch' |
|
103 | assert check_password(password, ret.password) == True , 'password mismatch' | |
103 | assert ret.email == email , 'field mismatch %s %s' % (ret.email, email) |
|
104 | assert ret.email == email , 'field mismatch %s %s' % (ret.email, email) | |
@@ -105,7 +106,34 b' class TestLoginController(TestController' | |||||
105 | assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname) |
|
106 | assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname) | |
106 |
|
107 | |||
107 |
|
108 | |||
|
109 | def test_forgot_password_wrong_mail(self): | |||
|
110 | response = self.app.post(url(controller='login', action='password_reset'), | |||
|
111 | {'email':'marcin@wrongmail.org', }) | |||
|
112 | ||||
|
113 | assert "That e-mail address doesn't exist" in response.body, 'Missing error message about wrong email' | |||
|
114 | ||||
|
115 | def test_forgot_password(self): | |||
|
116 | response = self.app.get(url(controller='login', action='password_reset')) | |||
|
117 | assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status | |||
|
118 | ||||
|
119 | username = 'test_password_reset_1' | |||
|
120 | password = 'qweqwe' | |||
|
121 | email = 'marcin@python-works.com' | |||
|
122 | name = 'passwd' | |||
|
123 | lastname = 'reset' | |||
|
124 | ||||
|
125 | response = self.app.post(url(controller='login', action='register'), | |||
|
126 | {'username':username, | |||
|
127 | 'password':password, | |||
|
128 | 'email':email, | |||
|
129 | 'name':name, | |||
|
130 | 'lastname':lastname}) | |||
|
131 | #register new user for email test | |||
|
132 | response = self.app.post(url(controller='login', action='password_reset'), | |||
|
133 | {'email':email, }) | |||
|
134 | print response.session['flash'] | |||
|
135 | assert 'You have successfully registered into hg-app' in response.session['flash'][0], 'No flash message about user registration' | |||
|
136 | assert 'Your new password was sent' in response.session['flash'][1], 'No flash message about password reset' | |||
108 |
|
137 | |||
109 |
|
138 | |||
110 |
|
139 | |||
111 |
|
@@ -23,7 +23,16 b' class TestSearchController(TestControlle' | |||||
23 |
|
23 | |||
24 | def test_normal_search(self): |
|
24 | def test_normal_search(self): | |
25 | self.log_user() |
|
25 | self.log_user() | |
26 |
response = self.app.get(url(controller='search', action='index'),{'q':'def |
|
26 | response = self.app.get(url(controller='search', action='index'), {'q':'def repo'}) | |
27 | print response.body |
|
27 | print response.body | |
28 |
assert ' |
|
28 | assert '10 results' in response.body, 'no message about proper search results' | |
|
29 | assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user' | |||
|
30 | ||||
29 |
|
|
31 | ||
|
32 | def test_repo_search(self): | |||
|
33 | self.log_user() | |||
|
34 | response = self.app.get(url(controller='search', action='index'), {'q':'repository:vcs_test def test'}) | |||
|
35 | print response.body | |||
|
36 | assert '4 results' in response.body, 'no message about proper search results' | |||
|
37 | assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user' | |||
|
38 |
@@ -3,6 +3,7 b' from pylons_app.tests import *' | |||||
3 | class TestSettingsController(TestController): |
|
3 | class TestSettingsController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='settings', action='index', |
|
7 | response = self.app.get(url(controller='settings', action='index', | |
7 | repo_name='vcs_test')) |
|
8 | repo_name='vcs_test')) | |
8 | # Test response... |
|
9 | # Test response... |
@@ -3,5 +3,6 b' from pylons_app.tests import *' | |||||
3 | class TestShortlogController(TestController): |
|
3 | class TestShortlogController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='shortlog', action='index',repo_name='vcs_test')) |
|
7 | response = self.app.get(url(controller='shortlog', action='index',repo_name='vcs_test')) | |
7 | # Test response... |
|
8 | # Test response... |
@@ -3,5 +3,6 b' from pylons_app.tests import *' | |||||
3 | class TestSummaryController(TestController): |
|
3 | class TestSummaryController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='summary', action='index',repo_name='vcs_test')) |
|
7 | response = self.app.get(url(controller='summary', action='index',repo_name='vcs_test')) | |
7 | # Test response... |
|
8 | # Test response... |
@@ -3,5 +3,6 b' from pylons_app.tests import *' | |||||
3 | class TestTagsController(TestController): |
|
3 | class TestTagsController(TestController): | |
4 |
|
4 | |||
5 | def test_index(self): |
|
5 | def test_index(self): | |
|
6 | self.log_user() | |||
6 | response = self.app.get(url(controller='tags', action='index',repo_name='vcs_test')) |
|
7 | response = self.app.get(url(controller='tags', action='index',repo_name='vcs_test')) | |
7 | # Test response... |
|
8 | # Test response... |
@@ -1,40 +1,25 b'' | |||||
1 | """Setup the pylons_app application""" |
|
1 | """Setup the pylons_app application""" | |
2 |
|
2 | |||
3 |
from os.path import dirname as dn |
|
3 | from os.path import dirname as dn | |
4 | from pylons_app.config.environment import load_environment |
|
4 | from pylons_app.config.environment import load_environment | |
5 | from pylons_app.lib.db_manage import DbManage |
|
5 | from pylons_app.lib.db_manage import DbManage | |
6 | import datetime |
|
|||
7 | from time import mktime |
|
|||
8 | import logging |
|
6 | import logging | |
9 | import os |
|
7 | import os | |
10 | import sys |
|
8 | import sys | |
11 | import tarfile |
|
|||
12 |
|
9 | |||
13 | log = logging.getLogger(__name__) |
|
10 | log = logging.getLogger(__name__) | |
14 |
|
11 | |||
15 | ROOT = dn(dn(os.path.realpath(__file__))) |
|
12 | ROOT = dn(dn(os.path.realpath(__file__))) | |
16 | sys.path.append(ROOT) |
|
13 | sys.path.append(ROOT) | |
17 |
|
14 | |||
|
15 | ||||
18 | def setup_app(command, conf, vars): |
|
16 | def setup_app(command, conf, vars): | |
19 | """Place any commands to setup pylons_app here""" |
|
17 | """Place any commands to setup pylons_app here""" | |
20 | log_sql = True |
|
18 | log_sql = True | |
21 | tests = False |
|
19 | tests = False | |
|
20 | REPO_TEST_PATH = None | |||
22 |
|
21 | |||
23 | dbname = os.path.split(conf['sqlalchemy.db1.url'])[-1] |
|
22 | dbname = os.path.split(conf['sqlalchemy.db1.url'])[-1] | |
24 | filename = os.path.split(conf.filename)[-1] |
|
|||
25 |
|
||||
26 | if filename == 'tests.ini': |
|
|||
27 | uniq_suffix = str(int(mktime(datetime.datetime.now().timetuple()))) |
|
|||
28 | REPO_TEST_PATH = '/tmp/hg_app_test_%s' % uniq_suffix |
|
|||
29 |
|
||||
30 | if not os.path.isdir(REPO_TEST_PATH): |
|
|||
31 | os.mkdir(REPO_TEST_PATH) |
|
|||
32 | cur_dir = dn(os.path.abspath(__file__)) |
|
|||
33 | tar = tarfile.open(jn(cur_dir,'tests',"vcs_test.tar.gz")) |
|
|||
34 | tar.extractall(REPO_TEST_PATH) |
|
|||
35 | tar.close() |
|
|||
36 |
|
||||
37 | tests = True |
|
|||
38 |
|
23 | |||
39 | dbmanage = DbManage(log_sql, dbname, tests) |
|
24 | dbmanage = DbManage(log_sql, dbname, tests) | |
40 | dbmanage.create_tables(override=True) |
|
25 | dbmanage.create_tables(override=True) |
@@ -8,7 +8,7 b' find_links = http://www.pylonshq.com/dow' | |||||
8 | [nosetests] |
|
8 | [nosetests] | |
9 | verbose=True |
|
9 | verbose=True | |
10 | verbosity=2 |
|
10 | verbosity=2 | |
11 |
with-pylons=test |
|
11 | with-pylons=test.ini | |
12 | detailed-errors=1 |
|
12 | detailed-errors=1 | |
13 |
|
13 | |||
14 | # Babel configuration |
|
14 | # Babel configuration |
@@ -20,12 +20,13 b' setup(' | |||||
20 | "SQLAlchemy>=0.6", |
|
20 | "SQLAlchemy>=0.6", | |
21 | "babel", |
|
21 | "babel", | |
22 | "Mako>=0.3.2", |
|
22 | "Mako>=0.3.2", | |
23 |
"vcs>=0.1. |
|
23 | "vcs>=0.1.5", | |
24 | "pygments>=1.3.0", |
|
24 | "pygments>=1.3.0", | |
25 | "mercurial>=1.6", |
|
25 | "mercurial>=1.6", | |
26 | "pysqlite", |
|
26 | "pysqlite", | |
27 |
"whoosh==1.0.0b1 |
|
27 | "whoosh==1.0.0b17", | |
28 | "py-bcrypt", |
|
28 | "py-bcrypt", | |
|
29 | "celery", | |||
29 | ], |
|
30 | ], | |
30 | setup_requires=["PasteScript>=1.6.3"], |
|
31 | setup_requires=["PasteScript>=1.6.3"], | |
31 | packages=find_packages(exclude=['ez_setup']), |
|
32 | packages=find_packages(exclude=['ez_setup']), |
@@ -1,28 +1,33 b'' | |||||
1 | ################################################################################ |
|
1 | ################################################################################ | |
2 | ################################################################################ |
|
2 | ################################################################################ | |
3 |
# |
|
3 | # hg-app - Pylons environment configuration # | |
4 | # # |
|
4 | # # | |
5 | # The %(here)s variable will be replaced with the parent directory of this file# |
|
5 | # The %(here)s variable will be replaced with the parent directory of this file# | |
6 | ################################################################################ |
|
6 | ################################################################################ | |
7 |
|
7 | |||
8 | [DEFAULT] |
|
8 | [DEFAULT] | |
9 | debug = true |
|
9 | debug = true | |
10 | ############################################ |
|
10 | ################################################################################ | |
11 | ## Uncomment and replace with the address ## |
|
11 | ## Uncomment and replace with the address which should receive ## | |
12 | ## which should receive any error reports ## |
|
12 | ## any error reports after application crash ## | |
13 | ############################################ |
|
13 | ## Additionally those settings will be used by hg-app mailing system ## | |
|
14 | ################################################################################ | |||
14 | #email_to = admin@localhost |
|
15 | #email_to = admin@localhost | |
|
16 | #error_email_from = paste_error@localhost | |||
|
17 | #app_email_from = hg-app-noreply@localhost | |||
|
18 | #error_message = | |||
|
19 | ||||
15 | #smtp_server = mail.server.com |
|
20 | #smtp_server = mail.server.com | |
16 | #error_email_from = paste_error@localhost |
|
|||
17 | #smtp_username = |
|
21 | #smtp_username = | |
18 | #smtp_password = |
|
22 | #smtp_password = | |
19 | #error_message = 'mercurial crash !' |
|
23 | #smtp_port = | |
|
24 | #smtp_use_tls = false | |||
20 |
|
25 | |||
21 | [server:main] |
|
26 | [server:main] | |
22 | ##nr of threads to spawn |
|
27 | ##nr of threads to spawn | |
23 | threadpool_workers = 5 |
|
28 | threadpool_workers = 5 | |
24 |
|
29 | |||
25 | ##max request before |
|
30 | ##max request before thread respawn | |
26 | threadpool_max_requests = 2 |
|
31 | threadpool_max_requests = 2 | |
27 |
|
32 | |||
28 | ##option to use threads of process |
|
33 | ##option to use threads of process | |
@@ -56,7 +61,7 b' beaker.cache.super_short_term.expire=10' | |||||
56 | ### BEAKER SESSION #### |
|
61 | ### BEAKER SESSION #### | |
57 | #################################### |
|
62 | #################################### | |
58 | ## Type of storage used for the session, current types are |
|
63 | ## Type of storage used for the session, current types are | |
59 |
## |
|
64 | ## "dbm", "file", "memcached", "database", and "memory". | |
60 | ## The storage uses the Container API |
|
65 | ## The storage uses the Container API | |
61 | ##that is also used by the cache system. |
|
66 | ##that is also used by the cache system. | |
62 | beaker.session.type = file |
|
67 | beaker.session.type = file |
General Comments 0
You need to be logged in to leave comments.
Login now