allura
修訂 | 70fa32ad2e434d4bb7f3a130da33da039a469976 (tree) |
---|---|
時間 | 2012-05-22 05:08:10 |
作者 | Yaroslav Luzin <jardev@gmai...> |
Commiter | Yaroslav Luzin |
Merge branch 'dev' into t50_hide_project_icon_placeholder
@@ -5,3 +5,4 @@ from smtp_server import SMTPServerCommand | ||
5 | 5 | from create_neighborhood import CreateNeighborhoodCommand |
6 | 6 | from create_trove_categories import CreateTroveCategoriesCommand |
7 | 7 | from set_neighborhood_features import SetNeighborhoodFeaturesCommand |
8 | +from rssfeeds import RssFeedsCommand |
@@ -0,0 +1,84 @@ | ||
1 | +import feedparser | |
2 | +import html2text | |
3 | +from bson import ObjectId | |
4 | + | |
5 | +import base | |
6 | + | |
7 | +from pylons import c | |
8 | + | |
9 | +from allura import model as M | |
10 | +from forgeblog import model as BM | |
11 | +from forgeblog import version | |
12 | +from forgeblog.main import ForgeBlogApp | |
13 | +from allura.lib import exceptions | |
14 | + | |
15 | + | |
16 | +class RssFeedsCommand(base.Command): | |
17 | + summary = 'Rss feed client' | |
18 | + parser = base.Command.standard_parser(verbose=True) | |
19 | + parser.add_option('-a', '--appid', dest='appid', default='', | |
20 | + help='application id') | |
21 | + parser.add_option('-u', '--username', dest='username', default='root', | |
22 | + help='poster username') | |
23 | + | |
24 | + def command(self): | |
25 | + self.basic_setup() | |
26 | + | |
27 | + user = M.User.query.get(username=self.options.username) | |
28 | + c.user = user | |
29 | + | |
30 | + self.prepare_feeds() | |
31 | + for appid in self.feed_dict: | |
32 | + for feed_url in self.feed_dict[appid]: | |
33 | + self.process_feed(appid, feed_url) | |
34 | + | |
35 | + def prepare_feeds(self): | |
36 | + feed_dict = {} | |
37 | + if self.options.appid != '': | |
38 | + gl_app = BM.Globals.query.get(app_config_id=ObjectId(self.options.appid)) | |
39 | + if not gl_app: | |
40 | + raise exceptions.NoSuchGlobalsError("The globals %s " \ | |
41 | + "could not be found in the database" % self.options.appid) | |
42 | + if len(gl_app.external_feeds) > 0: | |
43 | + feed_dict[gl_app.app_config_id] = gl_app.external_feeds | |
44 | + else: | |
45 | + for gl_app in BM.Globals.query.find().all(): | |
46 | + if len(gl_app.external_feeds) > 0: | |
47 | + feed_dict[gl_app.app_config_id] = gl_app.external_feeds | |
48 | + self.feed_dict = feed_dict | |
49 | + | |
50 | + def process_feed(self, appid, feed_url): | |
51 | + appconf = M.AppConfig.query.get(_id=appid) | |
52 | + if not appconf: | |
53 | + return | |
54 | + | |
55 | + c.project = appconf.project | |
56 | + app = ForgeBlogApp(c.project, appconf) | |
57 | + c.app = app | |
58 | + | |
59 | + base.log.info("Get feed: %s" % feed_url) | |
60 | + f = feedparser.parse(feed_url) | |
61 | + if f.bozo: | |
62 | + base.log.exception("%s: %s" % (feed_url, f.bozo_exception)) | |
63 | + return | |
64 | + for e in f.entries: | |
65 | + title = e.title | |
66 | + if 'content' in e: | |
67 | + content = u'' | |
68 | + for ct in e.content: | |
69 | + if ct.type != 'text/html': | |
70 | + content = u"%s<p>%s</p>" % (content, ct.value) | |
71 | + else: | |
72 | + content = content + ct.value | |
73 | + else: | |
74 | + content = e.summary | |
75 | + | |
76 | + content = u'%s <a href="%s">link</a>' % (content, e.link) | |
77 | + content = html2text.html2text(content, e.link) | |
78 | + | |
79 | + post = BM.BlogPost(title=title, text=content, app_config_id=appid, | |
80 | + tool_version={'blog': version.__version__}, | |
81 | + state='draft') | |
82 | + post.neighborhood_id=c.project.neighborhood_id | |
83 | + post.make_slug() | |
84 | + post.commit() |
@@ -23,7 +23,7 @@ from ming.orm.middleware import MingMiddleware | ||
23 | 23 | from allura.config.app_cfg import base_config |
24 | 24 | from allura.config.environment import load_environment |
25 | 25 | from allura.config.app_cfg import ForgeConfig |
26 | -from allura.lib.custom_middleware import StatsMiddleware | |
26 | +from allura.lib.custom_middleware import AlluraTimerMiddleware | |
27 | 27 | from allura.lib.custom_middleware import SSLMiddleware |
28 | 28 | from allura.lib.custom_middleware import StaticFilesMiddleware |
29 | 29 | from allura.lib.custom_middleware import CSRFMiddleware |
@@ -33,8 +33,8 @@ from allura.lib import helpers as h | ||
33 | 33 | |
34 | 34 | __all__ = ['make_app'] |
35 | 35 | |
36 | -# Use base_config to setup the necessary PasteDeploy application factory. | |
37 | -# make_base_app will wrap the TG2 app with all the middleware it needs. | |
36 | +# Use base_config to setup the necessary PasteDeploy application factory. | |
37 | +# make_base_app will wrap the TG2 app with all the middleware it needs. | |
38 | 38 | make_base_app = base_config.setup_tg_wsgi_app(load_environment) |
39 | 39 | |
40 | 40 |
@@ -55,13 +55,13 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
55 | 55 | :type full_stack: str or bool |
56 | 56 | :return: The allura application with all the relevant middleware |
57 | 57 | loaded. |
58 | - | |
58 | + | |
59 | 59 | This is the PasteDeploy factory for the allura application. |
60 | - | |
60 | + | |
61 | 61 | ``app_conf`` contains all the application-specific settings (those defined |
62 | 62 | under ``[app:main]``. |
63 | - | |
64 | - | |
63 | + | |
64 | + | |
65 | 65 | """ |
66 | 66 | # Run all the initialization code here |
67 | 67 | mimetypes.init( |
@@ -73,7 +73,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
73 | 73 | |
74 | 74 | # Configure EW variable provider |
75 | 75 | ew.render.TemplateEngine.register_variable_provider(get_tg_vars) |
76 | - | |
76 | + | |
77 | 77 | # Create base app |
78 | 78 | base_config = ForgeConfig(root) |
79 | 79 | load_environment = base_config.make_load_environment() |
@@ -86,7 +86,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
86 | 86 | load_environment(global_conf, app_conf) |
87 | 87 | |
88 | 88 | if config.get('zarkov.host'): |
89 | - try: | |
89 | + try: | |
90 | 90 | import zmq |
91 | 91 | except ImportError: |
92 | 92 | raise ImportError, "Unable to import the zmq library. Please"\ |
@@ -113,9 +113,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
113 | 113 | # Redirect 401 to the login page |
114 | 114 | app = LoginRedirectMiddleware(app) |
115 | 115 | # Add instrumentation |
116 | - if app_conf.get('stats.sample_rate', '0.25') != '0': | |
117 | - stats_config = dict(global_conf, **app_conf) | |
118 | - app = StatsMiddleware(app, stats_config) | |
116 | + app = AlluraTimerMiddleware(app, app_conf) | |
119 | 117 | # Clear cookies when the CSRF field isn't posted |
120 | 118 | if not app_conf.get('disable_csrf_protection'): |
121 | 119 | app = CSRFMiddleware(app, '_session_id') |
@@ -145,7 +143,7 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
145 | 143 | # the WSGI application's iterator is exhausted |
146 | 144 | app = RegistryManager(app, streaming=True) |
147 | 145 | return app |
148 | - | |
146 | + | |
149 | 147 | def set_scheme_middleware(app): |
150 | 148 | def SchemeMiddleware(environ, start_response): |
151 | 149 | if asbool(environ.get('HTTP_X_SFINC_SSL', 'false')): |
@@ -1,6 +1,8 @@ | ||
1 | 1 | import os |
2 | 2 | import json |
3 | 3 | import logging |
4 | +import re | |
5 | +import difflib | |
4 | 6 | from urllib import quote, unquote |
5 | 7 | from collections import defaultdict |
6 | 8 |
@@ -15,7 +17,6 @@ from ming.base import Object | ||
15 | 17 | from ming.orm import ThreadLocalORMSession, session |
16 | 18 | |
17 | 19 | import allura.tasks |
18 | -from allura.lib import patience | |
19 | 20 | from allura.lib import security |
20 | 21 | from allura.lib import helpers as h |
21 | 22 | from allura.lib import widgets as w |
@@ -61,7 +62,7 @@ class RepoRootController(BaseController): | ||
61 | 62 | |
62 | 63 | @with_trailing_slash |
63 | 64 | @expose('jinja:allura:templates/repo/fork.html') |
64 | - def fork(self, to_name=None, project_id=None): | |
65 | + def fork(self, project_id=None, mount_point=None, mount_label=None): | |
65 | 66 | # this shows the form and handles the submission |
66 | 67 | security.require_authenticated() |
67 | 68 | if not c.app.forkable: raise exc.HTTPNotFound |
@@ -70,10 +71,13 @@ class RepoRootController(BaseController): | ||
70 | 71 | ThreadLocalORMSession.close_all() |
71 | 72 | from_project = c.project |
72 | 73 | to_project = M.Project.query.get(_id=ObjectId(project_id)) |
73 | - if request.method != 'POST' or not to_name: | |
74 | + mount_label = mount_label or '%s - %s' % (c.project.name, from_repo.tool_name) | |
75 | + mount_point = (mount_point or from_project.shortname) | |
76 | + if request.method != 'POST' or not mount_point: | |
74 | 77 | return dict(from_repo=from_repo, |
75 | 78 | user_project=c.user.private_project(), |
76 | - to_name=to_name or '') | |
79 | + mount_point=mount_point, | |
80 | + mount_label=mount_label) | |
77 | 81 | else: |
78 | 82 | with h.push_config(c, project=to_project): |
79 | 83 | if not to_project.database_configured: |
@@ -81,10 +85,12 @@ class RepoRootController(BaseController): | ||
81 | 85 | security.require(security.has_access(to_project, 'admin')) |
82 | 86 | try: |
83 | 87 | to_project.install_app( |
84 | - from_repo.tool_name, to_name, | |
88 | + ep_name=from_repo.tool_name, | |
89 | + mount_point=mount_point, | |
90 | + mount_label=mount_label, | |
85 | 91 | cloned_from_project_id=from_project._id, |
86 | 92 | cloned_from_repo_id=from_repo._id) |
87 | - redirect(to_project.url()+to_name+'/') | |
93 | + redirect(to_project.url()+mount_point+'/') | |
88 | 94 | except exc.HTTPRedirection: |
89 | 95 | raise |
90 | 96 | except Exception, ex: |
@@ -514,7 +520,7 @@ class FileBrowser(BaseController): | ||
514 | 520 | b = self._blob |
515 | 521 | la = list(a) |
516 | 522 | lb = list(b) |
517 | - diff = ''.join(patience.unified_diff( | |
523 | + diff = ''.join(difflib.unified_diff( | |
518 | 524 | la, lb, |
519 | 525 | ('a' + apath).encode('utf-8'), |
520 | 526 | ('b' + b.path()).encode('utf-8'))) |
@@ -2,7 +2,7 @@ | ||
2 | 2 | |
3 | 3 | {% block title %}{{user.display_name}} / Profile{% endblock %} |
4 | 4 | |
5 | -{% block header %}{{c.project.homepage_title}}{% endblock %} | |
5 | +{% block header %}{{ user.display_name|default(user.username) }}{% endblock %} | |
6 | 6 | |
7 | 7 | {% block extra_css %} |
8 | 8 | <link rel="stylesheet" type="text/css" |
@@ -60,7 +60,7 @@ class Globals(object): | ||
60 | 60 | if asbool(config.get('solr.mock')): |
61 | 61 | self.solr = MockSOLR() |
62 | 62 | elif self.solr_server: |
63 | - self.solr = pysolr.Solr(self.solr_server) | |
63 | + self.solr = pysolr.Solr(self.solr_server) | |
64 | 64 | else: # pragma no cover |
65 | 65 | self.solr = None |
66 | 66 | self.use_queue = asbool(config.get('use_queue', False)) |
@@ -77,7 +77,7 @@ class Globals(object): | ||
77 | 77 | # Setup pygments |
78 | 78 | self.pygments_formatter = utils.LineAnchorCodeHtmlFormatter( |
79 | 79 | cssclass='codehilite', |
80 | - linenos='inline') | |
80 | + linenos='table') | |
81 | 81 | |
82 | 82 | # Setup Pypeline |
83 | 83 | self.pypeline_markup = pypeline_markup |
@@ -86,42 +86,42 @@ class Globals(object): | ||
86 | 86 | self.analytics = analytics.GoogleAnalytics(account=config.get('ga.account', 'UA-XXXXX-X')) |
87 | 87 | |
88 | 88 | self.icons = dict( |
89 | - admin = Icon('x', 'ico-admin'), | |
90 | - pencil = Icon('p', 'ico-pencil'), | |
91 | - help = Icon('h', 'ico-help'), | |
92 | - search = Icon('s', 'ico-search'), | |
93 | - history = Icon('N', 'ico-history'), | |
94 | - feed = Icon('f', 'ico-feed'), | |
95 | - mail = Icon('M', 'ico-mail'), | |
96 | - reply = Icon('w', 'ico-reply'), | |
97 | - tag = Icon('z', 'ico-tag'), | |
98 | - flag = Icon('^', 'ico-flag'), | |
99 | - undelete = Icon('+', 'ico-undelete'), | |
100 | - delete = Icon('#', 'ico-delete'), | |
101 | - close = Icon('D', 'ico-close'), | |
102 | - table = Icon('n', 'ico-table'), | |
103 | - stats = Icon('Y', 'ico-stats'), | |
104 | - pin = Icon('@', 'ico-pin'), | |
105 | - folder = Icon('o', 'ico-folder'), | |
106 | - fork = Icon('R', 'ico-fork'), | |
107 | - merge = Icon('J', 'ico-merge'), | |
108 | - plus = Icon('+', 'ico-plus'), | |
109 | - conversation = Icon('q', 'ico-conversation'), | |
110 | - group = Icon('g', 'ico-group'), | |
111 | - user = Icon('U', 'ico-user'), | |
112 | - secure = Icon('(', 'ico-lock'), | |
113 | - unsecure = Icon(')', 'ico-unlock'), | |
89 | + admin=Icon('x', 'ico-admin'), | |
90 | + pencil=Icon('p', 'ico-pencil'), | |
91 | + help=Icon('h', 'ico-help'), | |
92 | + search=Icon('s', 'ico-search'), | |
93 | + history=Icon('N', 'ico-history'), | |
94 | + feed=Icon('f', 'ico-feed'), | |
95 | + mail=Icon('M', 'ico-mail'), | |
96 | + reply=Icon('w', 'ico-reply'), | |
97 | + tag=Icon('z', 'ico-tag'), | |
98 | + flag=Icon('^', 'ico-flag'), | |
99 | + undelete=Icon('+', 'ico-undelete'), | |
100 | + delete=Icon('#', 'ico-delete'), | |
101 | + close=Icon('D', 'ico-close'), | |
102 | + table=Icon('n', 'ico-table'), | |
103 | + stats=Icon('Y', 'ico-stats'), | |
104 | + pin=Icon('@', 'ico-pin'), | |
105 | + folder=Icon('o', 'ico-folder'), | |
106 | + fork=Icon('R', 'ico-fork'), | |
107 | + merge=Icon('J', 'ico-merge'), | |
108 | + plus=Icon('+', 'ico-plus'), | |
109 | + conversation=Icon('q', 'ico-conversation'), | |
110 | + group=Icon('g', 'ico-group'), | |
111 | + user=Icon('U', 'ico-user'), | |
112 | + secure=Icon('(', 'ico-lock'), | |
113 | + unsecure=Icon(')', 'ico-unlock'), | |
114 | 114 | # Permissions |
115 | - perm_read = Icon('E', 'ico-focus'), | |
116 | - perm_update = Icon('0', 'ico-sync'), | |
117 | - perm_create = Icon('e', 'ico-config'), | |
118 | - perm_register = Icon('e', 'ico-config'), | |
119 | - perm_delete = Icon('-', 'ico-minuscirc'), | |
120 | - perm_tool = Icon('x', 'ico-config'), | |
121 | - perm_admin = Icon('(', 'ico-lock'), | |
122 | - perm_has_yes = Icon('3', 'ico-check'), | |
123 | - perm_has_no = Icon('d', 'ico-noentry'), | |
124 | - perm_has_inherit = Icon('2', 'ico-checkcircle'), | |
115 | + perm_read=Icon('E', 'ico-focus'), | |
116 | + perm_update=Icon('0', 'ico-sync'), | |
117 | + perm_create=Icon('e', 'ico-config'), | |
118 | + perm_register=Icon('e', 'ico-config'), | |
119 | + perm_delete=Icon('-', 'ico-minuscirc'), | |
120 | + perm_tool=Icon('x', 'ico-config'), | |
121 | + perm_admin=Icon('(', 'ico-lock'), | |
122 | + perm_has_yes=Icon('3', 'ico-check'), | |
123 | + perm_has_no=Icon('d', 'ico-noentry'), | |
124 | + perm_has_inherit=Icon('2', 'ico-checkcircle'), | |
125 | 125 | ) |
126 | 126 | |
127 | 127 | # Cache some loaded entry points |
@@ -1,21 +1,14 @@ | ||
1 | 1 | import os |
2 | 2 | import re |
3 | 3 | import logging |
4 | -from contextlib import contextmanager | |
5 | -from threading import local | |
6 | -from random import random | |
7 | 4 | |
8 | 5 | import tg |
9 | -import pylons | |
10 | 6 | import pkg_resources |
11 | -import markdown | |
12 | 7 | from paste import fileapp |
13 | -from paste.deploy.converters import asbool | |
14 | 8 | from pylons.util import call_wsgi_application |
15 | -from tg.controllers import DecoratedController | |
9 | +from timermiddleware import Timer, TimerMiddleware | |
16 | 10 | from webob import exc, Request |
17 | 11 | |
18 | -from allura.lib.stats import timing, StatsRecord | |
19 | 12 | from allura.lib import helpers as h |
20 | 13 | |
21 | 14 | log = logging.getLogger(__name__) |
@@ -151,57 +144,39 @@ class SSLMiddleware(object): | ||
151 | 144 | resp = req.get_response(self.app) |
152 | 145 | return resp(environ, start_response) |
153 | 146 | |
154 | -class StatsMiddleware(object): | |
155 | - | |
156 | - def __init__(self, app, config): | |
157 | - self.app = app | |
158 | - self.config = config | |
159 | - self.log = logging.getLogger('stats') | |
160 | - self.active = False | |
161 | - try: | |
162 | - self.sample_rate = config.get('stats.sample_rate', 0.25) | |
163 | - self.debug = asbool(config.get('debug', 'false')) | |
164 | - self.instrument_pymongo() | |
165 | - self.instrument_template() | |
166 | - self.active = True | |
167 | - except KeyError: | |
168 | - self.sample_rate = 0 | |
169 | - | |
170 | - def instrument_pymongo(self): | |
171 | - import pymongo.collection | |
172 | - import ming.odm | |
173 | - timing('mongo').decorate(pymongo.collection.Collection, | |
174 | - 'count find find_one') | |
175 | - timing('mongo').decorate(pymongo.cursor.Cursor, | |
176 | - 'count distinct explain hint limit next rewind' | |
177 | - ' skip sort where') | |
178 | - timing('ming').decorate(ming.odm.odmsession.ODMSession, | |
179 | - 'flush find get') | |
180 | - timing('ming').decorate(ming.odm.odmsession.ODMCursor, | |
181 | - 'next') | |
182 | - | |
183 | - def instrument_template(self): | |
147 | +class AlluraTimerMiddleware(TimerMiddleware): | |
148 | + def timers(self): | |
149 | + import genshi | |
184 | 150 | import jinja2 |
185 | - import genshi.template | |
186 | - timing('template').decorate(genshi.template.Template, | |
187 | - '_prepare _parse generate') | |
188 | - timing('render').decorate(genshi.Stream, | |
189 | - 'render') | |
190 | - timing('render').decorate(jinja2.Template, | |
191 | - 'render') | |
192 | - timing('markdown').decorate(markdown.Markdown, | |
193 | - 'convert') | |
194 | - | |
195 | - | |
196 | - def __call__(self, environ, start_response): | |
197 | - req = Request(environ) | |
198 | - req.environ['sf.stats'] = s = StatsRecord(req, random() < self.sample_rate) | |
199 | - with s.timing('total'): | |
200 | - resp = req.get_response(self.app, catch_exc_info=self.debug) | |
201 | - result = resp(environ, start_response) | |
202 | - if s.active: | |
203 | - self.log.info('Stats: %r', s) | |
204 | - from allura import model as M | |
205 | - M.Stats.make(s.asdict()).m.insert() | |
206 | - return result | |
207 | - | |
151 | + import markdown | |
152 | + import ming | |
153 | + import pymongo | |
154 | + import socket | |
155 | + import urllib2 | |
156 | + | |
157 | + return [ | |
158 | + Timer('markdown', markdown.Markdown, 'convert'), | |
159 | + Timer('ming', ming.odm.odmsession.ODMCursor, 'next'), | |
160 | + Timer('ming', ming.odm.odmsession.ODMSession, 'flush', 'find', | |
161 | + 'get'), | |
162 | + Timer('ming', ming.schema.Document, 'validate', | |
163 | + debug_each_call=False), | |
164 | + Timer('ming', ming.schema.FancySchemaItem, '_validate_required', | |
165 | + '_validate_fast_missing', '_validate_optional', | |
166 | + debug_each_call=False), | |
167 | + Timer('mongo', pymongo.collection.Collection, 'count', 'find', | |
168 | + 'find_one'), | |
169 | + Timer('mongo', pymongo.cursor.Cursor, 'count', 'distinct', | |
170 | + 'explain', 'hint', 'limit', 'next', 'rewind', 'skip', | |
171 | + 'sort', 'where'), | |
172 | + Timer('jinja', jinja2.Template, 'render', 'stream', 'generate'), | |
173 | + # urlopen and socket io may or may not overlap partially | |
174 | + Timer('urlopen', urllib2, 'urlopen'), | |
175 | + Timer('render', genshi.Stream, 'render'), | |
176 | + Timer('socket_read', socket._fileobject, 'read', 'readline', | |
177 | + 'readlines', debug_each_call=False), | |
178 | + Timer('socket_write', socket._fileobject, 'write', 'writelines', | |
179 | + 'flush', debug_each_call=False), | |
180 | + Timer('template', genshi.template.Template, '_prepare', '_parse', | |
181 | + 'generate'), | |
182 | + ] |
@@ -4,6 +4,7 @@ class ProjectOverlimitError(ForgeError): pass | ||
4 | 4 | class ToolError(ForgeError): pass |
5 | 5 | class NoSuchProjectError(ForgeError): pass |
6 | 6 | class NoSuchNeighborhoodError(ForgeError): pass |
7 | +class NoSuchGlobalsError(ForgeError): pass | |
7 | 8 | class MailError(ForgeError): pass |
8 | 9 | class AddressException(MailError): pass |
9 | 10 | class NoSuchNBFeatureError(ForgeError): pass |
@@ -142,8 +142,8 @@ def _make_xs(X, ids): | ||
142 | 142 | results = dict( |
143 | 143 | (r._id, r) |
144 | 144 | for r in X.query.find(dict(_id={'$in':ids}))) |
145 | - result = ( results.get(i) for i in ids ) | |
146 | - return ( r for r in result if r is not None ) | |
145 | + result = (results.get(i) for i in ids) | |
146 | + return (r for r in result if r is not None) | |
147 | 147 | |
148 | 148 | @contextmanager |
149 | 149 | def push_config(obj, **kw): |
@@ -158,14 +158,14 @@ def push_config(obj, **kw): | ||
158 | 158 | try: |
159 | 159 | yield obj |
160 | 160 | finally: |
161 | - for k,v in saved_attrs.iteritems(): | |
161 | + for k, v in saved_attrs.iteritems(): | |
162 | 162 | setattr(obj, k, v) |
163 | 163 | for k in new_attrs: |
164 | 164 | delattr(obj, k) |
165 | 165 | |
166 | 166 | def sharded_path(name, num_parts=2): |
167 | 167 | parts = [ |
168 | - name[:i+1] | |
168 | + name[:i + 1] | |
169 | 169 | for i in range(num_parts) ] |
170 | 170 | return '/'.join(parts) |
171 | 171 |
@@ -227,12 +227,12 @@ def encode_keys(d): | ||
227 | 227 | a valid kwargs argument''' |
228 | 228 | return dict( |
229 | 229 | (k.encode('utf-8'), v) |
230 | - for k,v in d.iteritems()) | |
230 | + for k, v in d.iteritems()) | |
231 | 231 | |
232 | 232 | def vardec(fun): |
233 | 233 | def vardec_hook(remainder, params): |
234 | 234 | new_params = variable_decode(dict( |
235 | - (k,v) for k,v in params.items() | |
235 | + (k, v) for k, v in params.items() | |
236 | 236 | if re_clean_vardec_key.match(k))) |
237 | 237 | params.update(new_params) |
238 | 238 | before_validate(vardec_hook)(fun) |
@@ -276,7 +276,7 @@ class DateTimeConverter(FancyValidator): | ||
276 | 276 | try: |
277 | 277 | return parse(value) |
278 | 278 | except ValueError: |
279 | - if self.if_invalid!=formencode.api.NoDefault: | |
279 | + if self.if_invalid != formencode.api.NoDefault: | |
280 | 280 | return self.if_invalid |
281 | 281 | else: |
282 | 282 | raise |
@@ -336,7 +336,7 @@ class ProxiedAttrMeta(type): | ||
336 | 336 | v.cls = cls |
337 | 337 | |
338 | 338 | class attrproxy(object): |
339 | - cls=None | |
339 | + cls = None | |
340 | 340 | def __init__(self, *attrs): |
341 | 341 | self.attrs = attrs |
342 | 342 |
@@ -413,7 +413,7 @@ def json_validation_error(controller, **kwargs): | ||
413 | 413 | errors=c.validation_exception.unpack_errors(), |
414 | 414 | value=c.validation_exception.value, |
415 | 415 | params=kwargs) |
416 | - response.status=400 | |
416 | + response.status = 400 | |
417 | 417 | return json.dumps(result, indent=2) |
418 | 418 | |
419 | 419 | def pop_user_notifications(user=None): |
@@ -429,8 +429,8 @@ def config_with_prefix(d, prefix): | ||
429 | 429 | '''Return a subdictionary keys with a given prefix, |
430 | 430 | with the prefix stripped |
431 | 431 | ''' |
432 | - plen=len(prefix) | |
433 | - return dict((k[plen:], v) for k,v in d.iteritems() | |
432 | + plen = len(prefix) | |
433 | + return dict((k[plen:], v) for k, v in d.iteritems() | |
434 | 434 | if k.startswith(prefix)) |
435 | 435 | |
436 | 436 | @contextmanager |
@@ -478,7 +478,7 @@ class log_action(object): | ||
478 | 478 | extra = kwargs.setdefault('extra', {}) |
479 | 479 | meta = kwargs.pop('meta', {}) |
480 | 480 | kwpairs = extra.setdefault('kwpairs', {}) |
481 | - for k,v in meta.iteritems(): | |
481 | + for k, v in meta.iteritems(): | |
482 | 482 | kwpairs['meta_%s' % k] = v |
483 | 483 | extra.update(self._make_extra()) |
484 | 484 | self._logger.log(level, self._action + ': ' + message, *args, **kwargs) |
@@ -500,7 +500,7 @@ class log_action(object): | ||
500 | 500 | |
501 | 501 | def warning(self, message, *args, **kwargs): |
502 | 502 | self.log(logging.EXCEPTION, message, *args, **kwargs) |
503 | - warn=warning | |
503 | + warn = warning | |
504 | 504 | |
505 | 505 | def _make_extra(self): |
506 | 506 | result = dict(self.extra_proto, action=self._action) |
@@ -542,7 +542,37 @@ def paging_sanitizer(limit, page, total_count, zero_based_pages=True): | ||
542 | 542 | page = min(max(int(page), (0 if zero_based_pages else 1)), max_page) |
543 | 543 | return limit, page |
544 | 544 | |
545 | -def render_any_markup(name, text, code_mode=False): | |
545 | + | |
546 | +def _add_inline_line_numbers_to_text(text): | |
547 | + markup_text = '<div class="codehilite"><pre>' | |
548 | + for line_num, line in enumerate(text.splitlines(), 1): | |
549 | + markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line) | |
550 | + markup_text = markup_text + '</pre></div>' | |
551 | + return markup_text | |
552 | + | |
553 | + | |
554 | +def _add_table_line_numbers_to_text(text): | |
555 | + def _prepend_whitespaces(num, max_num): | |
556 | + num, max_num = str(num), str(max_num) | |
557 | + diff = len(max_num) - len(num) | |
558 | + return ' ' * diff + num | |
559 | + | |
560 | + def _len_to_str_column(l, start=1): | |
561 | + max_num = l + start | |
562 | + return '\n'.join(map(_prepend_whitespaces, range(start, max_num), [max_num] * l)) | |
563 | + | |
564 | + lines = text.splitlines(True) | |
565 | + linenumbers = '<td class="linenos"><div class="linenodiv"><pre>' + _len_to_str_column(len(lines)) + '</pre></div></td>' | |
566 | + markup_text = '<table class="codehilitetable"><tbody><tr>' + linenumbers + '<td class="code"><div class="codehilite"><pre>' | |
567 | + for line_num, line in enumerate(lines, 1): | |
568 | + markup_text = markup_text + '<span id="l%s" class="code_block">%s</span>' % (line_num, line) | |
569 | + markup_text = markup_text + '</pre></div></td></tr></tbody></table>' | |
570 | + return markup_text | |
571 | + | |
572 | + | |
573 | +INLINE = 'inline' | |
574 | +TABLE = 'table' | |
575 | +def render_any_markup(name, text, code_mode=False, linenumbers_style=TABLE): | |
546 | 576 | """ |
547 | 577 | renders any markup format using the pypeline |
548 | 578 | Returns jinja-safe text |
@@ -552,14 +582,42 @@ def render_any_markup(name, text, code_mode=False): | ||
552 | 582 | else: |
553 | 583 | text = pylons.g.pypeline_markup.render(name, text) |
554 | 584 | if not pylons.g.pypeline_markup.can_render(name): |
555 | - if code_mode: | |
556 | - markup_text = '<div class="codehilite"><pre>' | |
557 | - line_num = 1 | |
558 | - for line in text.splitlines(): | |
559 | - markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line) | |
560 | - line_num += 1 | |
561 | - markup_text = markup_text + '</pre></div>' | |
562 | - text = markup_text | |
585 | + if code_mode and linenumbers_style == INLINE: | |
586 | + text = _add_inline_line_numbers_to_text(text) | |
587 | + elif code_mode and linenumbers_style == TABLE: | |
588 | + text = _add_table_line_numbers_to_text(text) | |
563 | 589 | else: |
564 | 590 | text = '<pre>%s</pre>' % text |
565 | 591 | return Markup(text) |
592 | + | |
593 | +# copied from jinja2 dev | |
594 | +# latest release, 2.6, implements this incorrectly | |
595 | +# can remove and use jinja2 implementation after upgrading to 2.7 | |
596 | +def do_filesizeformat(value, binary=False): | |
597 | + """Format the value like a 'human-readable' file size (i.e. 13 kB, | |
598 | +4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega, | |
599 | +Giga, etc.), if the second parameter is set to `True` the binary | |
600 | +prefixes are used (Mebi, Gibi). | |
601 | +""" | |
602 | + bytes = float(value) | |
603 | + base = binary and 1024 or 1000 | |
604 | + prefixes = [ | |
605 | + (binary and 'KiB' or 'kB'), | |
606 | + (binary and 'MiB' or 'MB'), | |
607 | + (binary and 'GiB' or 'GB'), | |
608 | + (binary and 'TiB' or 'TB'), | |
609 | + (binary and 'PiB' or 'PB'), | |
610 | + (binary and 'EiB' or 'EB'), | |
611 | + (binary and 'ZiB' or 'ZB'), | |
612 | + (binary and 'YiB' or 'YB') | |
613 | + ] | |
614 | + if bytes == 1: | |
615 | + return '1 Byte' | |
616 | + elif bytes < base: | |
617 | + return '%d Bytes' % bytes | |
618 | + else: | |
619 | + for i, prefix in enumerate(prefixes): | |
620 | + unit = base ** (i + 2) | |
621 | + if bytes < unit: | |
622 | + return '%.1f %s' % ((base * bytes / unit), prefix) | |
623 | + return '%.1f %s' % ((base * bytes / unit), prefix) |
@@ -1,205 +0,0 @@ | ||
1 | -import sys | |
2 | -import difflib | |
3 | -from itertools import chain | |
4 | - | |
5 | -class Region(object): | |
6 | - '''Simple utility class that keeps track of subsequences''' | |
7 | - __slots__=('seq', # sequence | |
8 | - 'l', # lower bound | |
9 | - 'h', # upper bound | |
10 | - ) | |
11 | - def __init__(self, seq, l=0, h=None): | |
12 | - if h is None: h = len(seq) | |
13 | - self.seq, self.l, self.h = seq, l, h | |
14 | - | |
15 | - def __iter__(self): | |
16 | - '''Iterates over the subsequence only''' | |
17 | - for i in xrange(self.l, self.h): | |
18 | - yield self.seq[i] | |
19 | - | |
20 | - def __getitem__(self, i): | |
21 | - '''works like getitem on the subsequence. Slices return new | |
22 | - regions.''' | |
23 | - if isinstance(i, slice): | |
24 | - start, stop, step = i.indices(len(self)) | |
25 | - assert step == 1 | |
26 | - return self.clone(l=self.l+start,h=self.l+stop) | |
27 | - elif i >= 0: | |
28 | - return self.seq[i+self.l] | |
29 | - else: | |
30 | - return self.seq[i+self.h] | |
31 | - | |
32 | - def __len__(self): | |
33 | - return self.h - self.l | |
34 | - | |
35 | - def __repr__(self): | |
36 | - if len(self) > 8: | |
37 | - srepr = '[%s,...]' % (','.join(repr(x) for x in self[:5])) | |
38 | - else: | |
39 | - srepr = repr(list(self)) | |
40 | - return '<Region (%s,%s): %s>' % (self.l, self.h, srepr) | |
41 | - | |
42 | - def clone(self, **kw): | |
43 | - '''Return a new Region based on this one with the | |
44 | - provided kw arguments replaced in the constructor. | |
45 | - ''' | |
46 | - kwargs = dict(seq=self.seq, l=self.l, h=self.h) | |
47 | - kwargs.update(kw) | |
48 | - return Region(**kwargs) | |
49 | - | |
50 | -def region_diff(ra, rb): | |
51 | - '''generator yielding up to two matching blocks, one at the | |
52 | - beginning of the region and one at the end. This function | |
53 | - mutates the a and b regions, removing any matched blocks. | |
54 | - ''' | |
55 | - # Yield match at the beginning | |
56 | - i = 0 | |
57 | - while i < len(ra) and i < len(rb) and ra[i] == rb[i]: | |
58 | - i += 1 | |
59 | - if i: | |
60 | - yield ra.l, rb.l, i | |
61 | - ra.l+=i | |
62 | - rb.l+=i | |
63 | - # Yield match at the end | |
64 | - j = 0 | |
65 | - while j < len(ra) and j < len(rb) and ra[-j-1]==rb[-j-1]: | |
66 | - j+=1 | |
67 | - if j: | |
68 | - yield ra.h-j, rb.h-j, j | |
69 | - ra.h-=j | |
70 | - rb.h-=j | |
71 | - | |
72 | -def unique(a): | |
73 | - '''generator yielding unique lines of a sequence and their positions''' | |
74 | - count = {} | |
75 | - for aa in a: | |
76 | - count[aa] = count.get(aa, 0) + 1 | |
77 | - for i, aa in enumerate(a): | |
78 | - if count[aa] == 1: yield aa, i | |
79 | - | |
80 | -def common_unique(a, b): | |
81 | - '''generator yielding pairs i,j where | |
82 | - a[i] == b[j] and a[i] and b[j] are unique within a and b, | |
83 | - in increasing j order.''' | |
84 | - uq_a = dict(unique(a)) | |
85 | - for bb, j in unique(b): | |
86 | - try: | |
87 | - yield uq_a[bb], j | |
88 | - except KeyError, ke: | |
89 | - continue | |
90 | - | |
91 | -def patience(seq): | |
92 | - stacks = [] | |
93 | - for i, j in seq: | |
94 | - last_top = None | |
95 | - for stack in stacks: | |
96 | - top_i, top_j, top_back = stack[-1] | |
97 | - if top_i > i: | |
98 | - stack.append((i, j, last_top)) | |
99 | - break | |
100 | - last_top = len(stack)-1 | |
101 | - else: | |
102 | - stacks.append([(i, j, last_top)]) | |
103 | - if not stacks: return [] | |
104 | - prev = len(stacks[-1])-1 | |
105 | - seq = [] | |
106 | - for stack in reversed(stacks): | |
107 | - top_i, top_j, top_back = stack[prev] | |
108 | - seq.append((top_i, top_j)) | |
109 | - prev = top_back | |
110 | - return reversed(seq) | |
111 | - | |
112 | -def match_core(a, b): | |
113 | - '''Returns blocks that match between sequences a and b as | |
114 | - (index_a, index_b, size) | |
115 | - ''' | |
116 | - ra = Region(a) | |
117 | - rb = Region(b) | |
118 | - # beginning/end match | |
119 | - for block in region_diff(ra,rb): yield block | |
120 | - # patience core | |
121 | - last_i = last_j = None | |
122 | - for i, j in chain( | |
123 | - patience(common_unique(ra, rb)), | |
124 | - [(ra.h, rb.h)]): | |
125 | - if last_i is not None: | |
126 | - for block in region_diff(ra[last_i:i], rb[last_j:j]): | |
127 | - yield block | |
128 | - last_i = i | |
129 | - last_j = j | |
130 | - | |
131 | -def diff_gen(a, b, opcode_gen): | |
132 | - '''Convert a sequence of SequenceMatcher opcodes to | |
133 | - unified diff-like output | |
134 | - ''' | |
135 | - def _iter(): | |
136 | - for op, i1, i2, j1, j2 in opcode_gen: | |
137 | - if op == 'equal': | |
138 | - yield ' ', Region(a, i1, i2) | |
139 | - if op in ('delete', 'replace'): | |
140 | - yield '- ', Region(a, i1, i2) | |
141 | - if op in ('replace', 'insert'): | |
142 | - yield '+ ', Region(b, j1, j2) | |
143 | - for prefix, rgn in _iter(): | |
144 | - for line in rgn: | |
145 | - yield prefix, line | |
146 | - | |
147 | -def unified_diff( | |
148 | - a, b, fromfile='', tofile='', fromfiledate='', | |
149 | - tofiledate='', n=3, lineterm='\n'): | |
150 | - started = False | |
151 | - for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): | |
152 | - if not started: | |
153 | - yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm) | |
154 | - yield '+++ %s %s%s' % (tofile, tofiledate, lineterm) | |
155 | - started = True | |
156 | - i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4] | |
157 | - yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm) | |
158 | - for tag, i1, i2, j1, j2 in group: | |
159 | - if tag == 'equal': | |
160 | - for line in a[i1:i2]: | |
161 | - yield ' ' + line | |
162 | - continue | |
163 | - if tag == 'replace' or tag == 'delete': | |
164 | - for line in a[i1:i2]: | |
165 | - yield '-' + line | |
166 | - if tag == 'replace' or tag == 'insert': | |
167 | - for line in b[j1:j2]: | |
168 | - yield '+' + line | |
169 | - | |
170 | -class SequenceMatcher(difflib.SequenceMatcher): | |
171 | - | |
172 | - def get_matching_blocks(self): | |
173 | - '''Uses patience diff algorithm to find matching blocks.''' | |
174 | - if self.matching_blocks is not None: | |
175 | - return self.matching_blocks | |
176 | - matching_blocks = list(match_core(self.a, self.b)) | |
177 | - matching_blocks.append((len(self.a), len(self.b), 0)) | |
178 | - self.matching_blocks = sorted(matching_blocks) | |
179 | - return self.matching_blocks | |
180 | - | |
181 | -def test(): # pragma no cover | |
182 | - if len(sys.argv) == 3: | |
183 | - fn_a = sys.argv[1] | |
184 | - fn_b = sys.argv[2] | |
185 | - else: | |
186 | - fn_a = 'a.c' | |
187 | - fn_b = 'b.c' | |
188 | - a = open(fn_a).readlines() | |
189 | - b = open(fn_b).readlines() | |
190 | - # print '====', fn_a | |
191 | - # sys.stdout.write(''.join(a)) | |
192 | - # print '====', fn_b | |
193 | - # sys.stdout.write(''.join(b)) | |
194 | - sm = SequenceMatcher(None, a, b) | |
195 | - # print 'Patience opcodes:', sm.get_opcodes() | |
196 | - print ''.join(unified_diff(a, b)) #pragma:printok | |
197 | - # for prefix, line in diff_gen(a, b, sm.get_opcodes()): | |
198 | - # sys.stdout.write(''.join((prefix, line))) | |
199 | - # sm = difflib.SequenceMatcher(None, a, b) | |
200 | - # print 'Difflib opcodes:', sm.get_opcodes() | |
201 | - # for prefix, line in diff_gen(a, b, sm.get_opcodes()): | |
202 | - # sys.stdout.write(''.join((prefix, line))) | |
203 | - | |
204 | -if __name__ == '__main__': # pragma no cover | |
205 | - test() |
@@ -121,7 +121,7 @@ class Notification(MappedClass): | ||
121 | 121 | file_info.file.seek(0, 2) |
122 | 122 | bytecount = file_info.file.tell() |
123 | 123 | file_info.file.seek(0) |
124 | - text = "%s\n%s (%s bytes in %s)" % (text, file_info.filename, bytecount, file_info.type) | |
124 | + text = "%s\n\n\nAttachment: %s (%s; %s)" % (text, file_info.filename, h.do_filesizeformat(bytecount), file_info.type) | |
125 | 125 | |
126 | 126 | subject = post.subject or '' |
127 | 127 | if post.parent_id and not subject.lower().startswith('re:'): |
@@ -123,6 +123,7 @@ class Project(MappedClass): | ||
123 | 123 | shortname = FieldProperty(str) |
124 | 124 | name=FieldProperty(str) |
125 | 125 | notifications_disabled = FieldProperty(bool) |
126 | + suppress_emails = FieldProperty(bool) | |
126 | 127 | show_download_button=FieldProperty(S.Deprecated) |
127 | 128 | short_description=FieldProperty(str, if_missing='') |
128 | 129 | summary=FieldProperty(str, if_missing='') |
@@ -388,11 +389,17 @@ class Project(MappedClass): | ||
388 | 389 | delta_ordinal = 0 |
389 | 390 | max_ordinal = 0 |
390 | 391 | neighborhood_admin_mode = False |
392 | + | |
393 | + if self.is_user_project: | |
394 | + entries.append({'ordinal': delta_ordinal, 'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")}) | |
395 | + max_ordinal = delta_ordinal | |
396 | + delta_ordinal = delta_ordinal + 1 | |
397 | + | |
391 | 398 | if self == self.neighborhood.neighborhood_project: |
392 | - delta_ordinal = 1 | |
399 | + entries.append({'ordinal':delta_ordinal, 'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")}) | |
400 | + max_ordinal = delta_ordinal | |
401 | + delta_ordinal = delta_ordinal + 1 | |
393 | 402 | neighborhood_admin_mode = True |
394 | - entries.append({'ordinal':0,'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")}) | |
395 | - | |
396 | 403 | |
397 | 404 | for sub in self.direct_subprojects: |
398 | 405 | ordinal = sub.ordinal + delta_ordinal |
@@ -417,9 +424,6 @@ class Project(MappedClass): | ||
417 | 424 | entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Moderate', "%s_moderate/" % self.neighborhood.url(), ui_icon="tool-admin")}) |
418 | 425 | max_ordinal += 1 |
419 | 426 | |
420 | - if self.is_user_project: | |
421 | - entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")}) | |
422 | - | |
423 | 427 | entries = sorted(entries, key=lambda e: e['ordinal']) |
424 | 428 | for e in entries: |
425 | 429 | sitemap.children.append(e['entry']) |
@@ -5,6 +5,7 @@ import mimetypes | ||
5 | 5 | import logging |
6 | 6 | import string |
7 | 7 | import re |
8 | +from difflib import SequenceMatcher | |
8 | 9 | from hashlib import sha1 |
9 | 10 | from datetime import datetime |
10 | 11 | from collections import defaultdict |
@@ -20,7 +21,6 @@ from ming.utils import LazyProperty | ||
20 | 21 | from ming.orm import FieldProperty, session, Mapper |
21 | 22 | from ming.orm.declarative import MappedClass |
22 | 23 | |
23 | -from allura.lib.patience import SequenceMatcher | |
24 | 24 | from allura.lib import helpers as h |
25 | 25 | from allura.lib import utils |
26 | 26 |
@@ -20,7 +20,7 @@ SEE: https://groups.google.com/group/compass-users/browse_thread/thread/df09f674 | ||
20 | 20 | * Color variables for the theme. |
21 | 21 | * |
22 | 22 | */ |
23 | -/* | |
23 | +/* | |
24 | 24 | * Your run-of-the-mill reset CSS, inspired by: |
25 | 25 | * |
26 | 26 | * yui.yahooapis.com/2.8.1/build/base/base.css |
@@ -99,7 +99,7 @@ input, select { | ||
99 | 99 | background: #fff; |
100 | 100 | } |
101 | 101 | |
102 | -/* | |
102 | +/* | |
103 | 103 | * Setup a minimal, baseline CSS, layered on top of a reset |
104 | 104 | * to define the default styles we've come to expect. Inspired by: |
105 | 105 | * |
@@ -245,7 +245,7 @@ h2.title, #site-header nav { | ||
245 | 245 | font-weight: normal; |
246 | 246 | } |
247 | 247 | |
248 | -/* | |
248 | +/* | |
249 | 249 | * General CSS rules governing high-level elements. |
250 | 250 | * |
251 | 251 | */ |
@@ -380,7 +380,7 @@ blockquote { | ||
380 | 380 | padding-left: 1em; |
381 | 381 | } |
382 | 382 | |
383 | -/* | |
383 | +/* | |
384 | 384 | * Style elements in the main header and footer areas. |
385 | 385 | * |
386 | 386 | */ |
@@ -2707,3 +2707,43 @@ h1.title .viewer:hover { | ||
2707 | 2707 | .neighborhood_feed_entry h3 { |
2708 | 2708 | font-size: 1.1em; |
2709 | 2709 | } |
2710 | + | |
2711 | +/*linenumbers in codeblock viewer style*/ | |
2712 | + | |
2713 | +table.codehilitetable { | |
2714 | + background: #F8F8F8; | |
2715 | + margin-left:0px; | |
2716 | +} | |
2717 | + | |
2718 | +td.linenos { | |
2719 | + width:auto; | |
2720 | + padding: 0; | |
2721 | +} | |
2722 | +div.linenodiv { | |
2723 | + | |
2724 | +} | |
2725 | +td.linenos pre { | |
2726 | + font-size: 100%; | |
2727 | + padding: 1px; | |
2728 | + padding-left: 7px; | |
2729 | + padding-right: 5px; | |
2730 | + margin-left: 15px; | |
2731 | + background-color: #EBEBEB; | |
2732 | + color: #555; | |
2733 | + border-right: solid 1px #DDD; | |
2734 | +} | |
2735 | +td.code { | |
2736 | + padding-left: 0px; | |
2737 | + width:100%; | |
2738 | +} | |
2739 | + | |
2740 | +div.codehilite pre { | |
2741 | + padding-left: 0px; | |
2742 | + padding-top:10px; | |
2743 | + padding-bottom:10px; | |
2744 | +} | |
2745 | +div.codehilite pre div.code_block { | |
2746 | + padding-left:10px; | |
2747 | + width: 97%; | |
2748 | +} | |
2749 | + |
@@ -66,4 +66,9 @@ | ||
66 | 66 | .codehilite div { margin:0; padding: 0; } |
67 | 67 | .codehilite .code_block { width:100%; } |
68 | 68 | .codehilite .code_block:hover { background-color: #ffff99; } |
69 | -.codehilite .lineno { background-color: #ebebeb; display:inline-block; padding:0 .5em; border-width: 0 1px 0 0; border-style: solid; border-color: #ddd; } | |
69 | +.codehilite .lineno { background-color: #ebebeb; | |
70 | + display:inline-block; | |
71 | + padding:0 .5em; | |
72 | + border-width: 0 1px 0 0; | |
73 | + border-style: solid; | |
74 | + border-color: #ddd; } |
@@ -1,10 +1,13 @@ | ||
1 | 1 | import shutil |
2 | 2 | import logging |
3 | +import traceback | |
3 | 4 | |
4 | 5 | from pylons import c |
5 | 6 | |
6 | 7 | from allura.lib.decorators import task |
7 | 8 | from allura.lib.repository import RepositoryApp |
9 | +from allura.lib import helpers as h | |
10 | +from allura.tasks.mail_tasks import sendmail | |
8 | 11 | |
9 | 12 | @task |
10 | 13 | def init(**kwargs): |
@@ -20,15 +23,49 @@ def clone( | ||
20 | 23 | cloned_from_path, |
21 | 24 | cloned_from_name, |
22 | 25 | cloned_from_url): |
23 | - from allura import model as M | |
24 | - c.app.repo.init_as_clone( | |
25 | - cloned_from_path, | |
26 | - cloned_from_name, | |
27 | - cloned_from_url) | |
28 | - M.Notification.post_user( | |
29 | - c.user, c.app.repo, 'created', | |
30 | - text='Repository %s/%s created' % ( | |
31 | - c.project.shortname, c.app.config.options.mount_point)) | |
26 | + try: | |
27 | + from allura import model as M | |
28 | + c.app.repo.init_as_clone( | |
29 | + cloned_from_path, | |
30 | + cloned_from_name, | |
31 | + cloned_from_url) | |
32 | + M.Notification.post_user( | |
33 | + c.user, c.app.repo, 'created', | |
34 | + text='Repository %s/%s created' % ( | |
35 | + c.project.shortname, c.app.config.options.mount_point)) | |
36 | + if not c.project.suppress_emails: | |
37 | + sendmail( | |
38 | + destinations=[str(c.user._id)], | |
39 | + fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>', | |
40 | + reply_to=u'noreply@in.sf.net', | |
41 | + subject=u'SourceForge Repo Clone Complete', | |
42 | + message_id=h.gen_message_id(), | |
43 | + text=u''.join([ | |
44 | + u'Clone of repo %s in project %s from %s is complete. Your repo is now ready to use.\n' | |
45 | + ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url)) | |
46 | + except: | |
47 | + sendmail( | |
48 | + destinations=['sfengineers@geek.net'], | |
49 | + fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>', | |
50 | + reply_to=u'noreply@in.sf.net', | |
51 | + subject=u'SourceForge Repo Clone Failure', | |
52 | + message_id=h.gen_message_id(), | |
53 | + text=u''.join([ | |
54 | + u'Clone of repo %s in project %s from %s failed.\n', | |
55 | + u'\n', | |
56 | + u'%s', | |
57 | + ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url, traceback.format_exc())) | |
58 | + if not c.project.suppress_emails: | |
59 | + sendmail( | |
60 | + destinations=[str(c.user._id)], | |
61 | + fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>', | |
62 | + reply_to=u'noreply@in.sf.net', | |
63 | + subject=u'SourceForge Repo Clone Failed', | |
64 | + message_id=h.gen_message_id(), | |
65 | + text=u''.join([ | |
66 | + u'Clone of repo %s in project %s from %s failed. ', | |
67 | + u'The SourceForge engineering team has been notified.\n', | |
68 | + ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url)) | |
32 | 69 | |
33 | 70 | @task |
34 | 71 | def reclone(*args, **kwargs): |
@@ -70,7 +70,7 @@ | ||
70 | 70 | <div class="clip grid-19"> |
71 | 71 | <h3><span class="ico-l"><b data-icon="{{g.icons['table'].char}}" class="ico {{g.icons['table'].css}}"></b> {{h.really_unicode(blob.name)}}</span></h3> |
72 | 72 | {% if blob.has_pypeline_view %} |
73 | - {{h.render_any_markup(blob.name, blob.text, code_mode=True)}} | |
73 | + {{h.render_any_markup(blob.name, blob.text, code_mode=True, linenumbers_style=h.TABLE)}} | |
74 | 74 | {% else %} |
75 | 75 | {{g.highlight(blob.text, filename=blob.name)}} |
76 | 76 | {% endif %} |
@@ -16,9 +16,13 @@ | ||
16 | 16 | {% endfor %} |
17 | 17 | </select> |
18 | 18 | </div> |
19 | - <label class="grid-4">Repository Name:</label> | |
19 | + <label class="grid-4">Label:</label> | |
20 | 20 | <div class="grid-15"> |
21 | - <input type="text" name="to_name" value="{{to_name}}"/> | |
21 | + <input type="text" name="mount_label" value="{{mount_label}}"/> | |
22 | + </div> | |
23 | + <label class="grid-4">Mount point:</label> | |
24 | + <div class="grid-15"> | |
25 | + <input type="text" name="mount_point" value="{{mount_point}}"/> | |
22 | 26 | </div> |
23 | 27 | <label class="grid-4"> </label> |
24 | 28 | <div class="grid-15"> |
@@ -8,6 +8,7 @@ class TestUserProfile(TestController): | ||
8 | 8 | @td.with_user_project('test-admin') |
9 | 9 | def test_profile(self): |
10 | 10 | response = self.app.get('/u/test-admin/profile/') |
11 | + assert '<h2 class="dark title">Test Admin' in response | |
11 | 12 | assert 'OpenIDs' in response |
12 | 13 | response = self.app.get('/u/test-admin/profile/configuration') |
13 | 14 | assert 'Configure Dashboard' in response |
@@ -167,7 +167,7 @@ def test_attachment_methods(): | ||
167 | 167 | p = t.post(text=u'test message', forum= None, subject= '', file_info=fs) |
168 | 168 | ThreadLocalORMSession.flush_all() |
169 | 169 | n = M.Notification.query.get(subject=u'[test:wiki] Test comment notification') |
170 | - assert u'test message\nfake.txt (37 bytes in text/plain)'==n.text | |
170 | + assert_equals(u'test message\n\n\nAttachment: fake.txt (37 Bytes; text/plain)', n.text) | |
171 | 171 | |
172 | 172 | @with_setup(setUp, tearDown) |
173 | 173 | def test_discussion_delete(): |
@@ -1,11 +1,13 @@ | ||
1 | 1 | from nose.tools import assert_raises |
2 | 2 | |
3 | +from ming.orm.ormsession import ThreadLocalORMSession | |
4 | + | |
3 | 5 | from alluratest.controller import setup_basic_test, setup_global_objects |
4 | -from allura.command import script, set_neighborhood_features | |
6 | +from allura.command import script, set_neighborhood_features, rssfeeds | |
5 | 7 | from allura import model as M |
8 | +from forgeblog import model as BM | |
6 | 9 | from allura.lib.exceptions import InvalidNBFeatureValueError |
7 | 10 | |
8 | - | |
9 | 11 | test_config = 'test.ini#main' |
10 | 12 | |
11 | 13 | class EmptyClass(object): pass |
@@ -116,3 +118,21 @@ def test_set_neighborhood_css(): | ||
116 | 118 | assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', '2.8']) |
117 | 119 | assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'None']) |
118 | 120 | assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'True']) |
121 | + | |
122 | +def test_pull_rss_feeds(): | |
123 | + base_app = M.AppConfig.query.find().all()[0] | |
124 | + tmp_app = M.AppConfig(tool_name=u'Blog', discussion_id=base_app.discussion_id, | |
125 | + project_id=base_app.project_id, | |
126 | + options={u'ordinal': 0, u'show_right_bar': True, | |
127 | + u'project_name': base_app.project.name, | |
128 | + u'mount_point': u'blog', | |
129 | + u'mount_label': u'Blog'}) | |
130 | + new_external_feeds = ['http://wordpress.org/news/feed/'] | |
131 | + BM.Globals(app_config_id=tmp_app._id, external_feeds=new_external_feeds) | |
132 | + ThreadLocalORMSession.flush_all() | |
133 | + | |
134 | + cmd = rssfeeds.RssFeedsCommand('pull-rss-feeds') | |
135 | + cmd.run([test_config, '-a', tmp_app._id]) | |
136 | + cmd.command() | |
137 | + | |
138 | + assert len(BM.BlogPost.query.find({'app_config_id': tmp_app._id}).all()) > 0 |
@@ -1,76 +0,0 @@ | ||
1 | -from os import path, environ | |
2 | -from collections import defaultdict | |
3 | - | |
4 | -from allura.lib import patience | |
5 | - | |
6 | -def text2lines(text): | |
7 | - return [l + '\n' for l in text.split('\n')] | |
8 | - | |
9 | -def test_region(): | |
10 | - r = patience.Region('foobar') | |
11 | - r2 = r.clone() | |
12 | - assert id(r) != id(r2) | |
13 | - assert '-'.join(r) == '-'.join(r2) | |
14 | - subr = r[1:5] | |
15 | - assert type(subr) is type(r) | |
16 | - assert ''.join(subr) == ''.join(r)[1:5] | |
17 | - repr(r) | |
18 | - repr(patience.Region('fffffffffffffffffffffffffffffffffffffffff')) | |
19 | - | |
20 | -def test_unified_diff(): | |
21 | - text1 = '''\ | |
22 | -from paste.deploy import loadapp | |
23 | -from paste.deploy import loadapp | |
24 | -from paste.deploy import loadapp | |
25 | -from paste.deploy import loadapp | |
26 | -from paste.script.appinstall import SetupCommand | |
27 | -from paste.script.appinstall import SetupCommand | |
28 | -from paste.script.appinstall import SetupCommand | |
29 | -from paste.script.appinstall import SetupCommand | |
30 | -from paste.deploy import appconfig | |
31 | -''' | |
32 | - text2 = '''\ | |
33 | -from paste.deploy import loadapp | |
34 | -from paste.deploy import loadapp | |
35 | -from paste.deploy import loadapp | |
36 | -from paste.deploy import loadapp | |
37 | -from paste.script.appinstall import SetupCommand2 | |
38 | -from paste.script.appinstall import SetupCommand3 | |
39 | -from paste.script.appinstall import SetupCommand4 | |
40 | -from paste.deploy import appconfig | |
41 | -''' | |
42 | - line_uni_diff = '''\ | |
43 | - from paste.deploy import loadapp | |
44 | - from paste.deploy import loadapp | |
45 | - from paste.deploy import loadapp | |
46 | --from paste.script.appinstall import SetupCommand | |
47 | --from paste.script.appinstall import SetupCommand | |
48 | --from paste.script.appinstall import SetupCommand | |
49 | --from paste.script.appinstall import SetupCommand | |
50 | -+from paste.script.appinstall import SetupCommand2 | |
51 | -+from paste.script.appinstall import SetupCommand3 | |
52 | -+from paste.script.appinstall import SetupCommand4 | |
53 | - from paste.deploy import appconfig''' | |
54 | - | |
55 | - line_diff = '''\ | |
56 | - from paste.deploy import loadapp | |
57 | -''' + line_uni_diff | |
58 | - | |
59 | - lines1 = text2lines(text1) | |
60 | - lines2 = text2lines(text2) | |
61 | - diff = patience.unified_diff(lines1, lines2) | |
62 | - diff = ''.join(diff) | |
63 | - assert diff == '''\ | |
64 | ---- | |
65 | -+++ | |
66 | -@@ -2,9 +2,8 @@ | |
67 | -%s | |
68 | - | |
69 | -''' % line_uni_diff, '=' + diff + '=' | |
70 | - | |
71 | - sm = patience.SequenceMatcher(None, lines1, lines2) | |
72 | - buf = '' | |
73 | - for prefix, line in patience.diff_gen(lines1, lines2, sm.get_opcodes()): | |
74 | - assert prefix[1] == ' ' | |
75 | - buf += prefix[0] + line | |
76 | - assert buf == line_diff + '\n \n', '=' + buf + '=' |
@@ -113,6 +113,7 @@ setup( | ||
113 | 113 | create-neighborhood = allura.command:CreateNeighborhoodCommand |
114 | 114 | create-trove-categories = allura.command:CreateTroveCategoriesCommand |
115 | 115 | set-neighborhood-features = allura.command:SetNeighborhoodFeaturesCommand |
116 | + pull-rss-feeds = allura.command.rssfeeds:RssFeedsCommand | |
116 | 117 | |
117 | 118 | [easy_widgets.resources] |
118 | 119 | ew_resources=allura.config.resources:register_ew_resources |
@@ -75,7 +75,7 @@ scm.clone.svn = svn checkout --username=$username $source_url $dest_path | ||
75 | 75 | |
76 | 76 | scm.repos.root = /tmp |
77 | 77 | |
78 | -stats.sample_rate=0 | |
78 | +#stats.sample_rate = 0 | |
79 | 79 | |
80 | 80 | disable_csrf_protection=1 |
81 | 81 |
@@ -12,12 +12,14 @@ from pylons import g, c, request, response | ||
12 | 12 | from formencode import validators |
13 | 13 | from webob import exc |
14 | 14 | |
15 | +from ming.orm import session | |
16 | + | |
15 | 17 | # Pyforge-specific imports |
16 | 18 | from allura.app import Application, ConfigOption, SitemapEntry |
17 | 19 | from allura.app import DefaultAdminController |
18 | 20 | from allura.lib import helpers as h |
19 | 21 | from allura.lib.search import search |
20 | -from allura.lib.decorators import require_post | |
22 | +from allura.lib.decorators import require_post, Property | |
21 | 23 | from allura.lib.security import has_access, require_access |
22 | 24 | from allura.lib import widgets as w |
23 | 25 | from allura.lib.widgets.subscriptions import SubscribeForm |
@@ -56,6 +58,7 @@ class ForgeBlogApp(Application): | ||
56 | 58 | ordinal=14 |
57 | 59 | installable=True |
58 | 60 | config_options = Application.config_options |
61 | + default_external_feeds = [] | |
59 | 62 | icons={ |
60 | 63 | 24:'images/blog_24.png', |
61 | 64 | 32:'images/blog_32.png', |
@@ -67,6 +70,24 @@ class ForgeBlogApp(Application): | ||
67 | 70 | self.root = RootController() |
68 | 71 | self.admin = BlogAdminController(self) |
69 | 72 | |
73 | + @Property | |
74 | + def external_feeds_list(): | |
75 | + def fget(self): | |
76 | + globals = BM.Globals.query.get(app_config_id=self.config._id) | |
77 | + if globals is not None: | |
78 | + external_feeds = globals.external_feeds | |
79 | + else: | |
80 | + external_feeds = self.default_external_feeds | |
81 | + return external_feeds | |
82 | + def fset(self, new_external_feeds): | |
83 | + globals = BM.Globals.query.get(app_config_id=self.config._id) | |
84 | + if globals is not None: | |
85 | + globals.external_feeds = new_external_feeds | |
86 | + elif len(new_external_feeds) > 0: | |
87 | + globals = BM.Globals(app_config_id=self.config._id, external_feeds=new_external_feeds) | |
88 | + if globals is not None: | |
89 | + session(globals).flush() | |
90 | + | |
70 | 91 | @property |
71 | 92 | @h.exceptionless([], log) |
72 | 93 | def sitemap(self): |
@@ -94,7 +115,12 @@ class ForgeBlogApp(Application): | ||
94 | 115 | return links |
95 | 116 | |
96 | 117 | def admin_menu(self): |
97 | - return super(ForgeBlogApp, self).admin_menu(force_options=True) | |
118 | + admin_url = c.project.url() + 'admin/' + self.config.options.mount_point + '/' | |
119 | + # temporarily disabled until some bugs are fixed | |
120 | + links = []#[SitemapEntry('External feeds', admin_url + 'exfeed', className='admin_modal')] | |
121 | + links += super(ForgeBlogApp, self).admin_menu(force_options=True) | |
122 | + return links | |
123 | + #return super(ForgeBlogApp, self).admin_menu(force_options=True) | |
98 | 124 | |
99 | 125 | def install(self, project): |
100 | 126 | 'Set up any default permissions and roles here' |
@@ -170,7 +196,7 @@ class RootController(BaseController): | ||
170 | 196 | require_access(c.app, 'write') |
171 | 197 | now = datetime.utcnow() |
172 | 198 | post = dict( |
173 | - state='draft') | |
199 | + state='published') | |
174 | 200 | c.form = W.new_post_form |
175 | 201 | return dict(post=post) |
176 | 202 |
@@ -359,3 +385,34 @@ class BlogAdminController(DefaultAdminController): | ||
359 | 385 | self.app.config.options['show_discussion'] = show_discussion and True or False |
360 | 386 | flash('Blog options updated') |
361 | 387 | redirect(h.really_unicode(c.project.url()+'admin/tools').encode('utf-8')) |
388 | + | |
389 | + @without_trailing_slash | |
390 | + @expose('jinja:forgeblog:templates/blog/admin_exfeed.html') | |
391 | + def exfeed(self): | |
392 | + #self.app.external_feeds_list = ['feed1', 'feed2'] | |
393 | + #log.info("EXFEED: %s" % self.app.external_feeds_list) | |
394 | + feeds_list = [] | |
395 | + for feed in self.app.external_feeds_list: | |
396 | + feeds_list.append(feed) | |
397 | + return dict(app=self.app, | |
398 | + feeds_list=feeds_list, | |
399 | + allow_config=has_access(self.app, 'configure')()) | |
400 | + | |
401 | + @without_trailing_slash | |
402 | + @expose() | |
403 | + @require_post() | |
404 | + def set_exfeed(self, **kw): | |
405 | + new_exfeed = kw.get('new_exfeed', None) | |
406 | + exfeed_val = kw.get('exfeed', []) | |
407 | + if type(exfeed_val) == unicode: | |
408 | + exfeed_list = [] | |
409 | + exfeed_list.append(exfeed_val) | |
410 | + else: | |
411 | + exfeed_list = exfeed_val | |
412 | + | |
413 | + if new_exfeed is not None and new_exfeed != '': | |
414 | + exfeed_list.append(new_exfeed) | |
415 | + | |
416 | + self.app.external_feeds_list = exfeed_list | |
417 | + flash('External feeds updated') | |
418 | + redirect(c.project.url()+'admin/tools') |
@@ -1 +1 @@ | ||
1 | -from blog import BlogPost, Attachment, BlogPostSnapshot | |
1 | +from blog import Globals, BlogPost, Attachment, BlogPostSnapshot |
@@ -1,3 +1,4 @@ | ||
1 | +import difflib | |
1 | 2 | from datetime import datetime |
2 | 3 | from random import randint |
3 | 4 |
@@ -9,13 +10,28 @@ from pymongo.errors import DuplicateKeyError | ||
9 | 10 | |
10 | 11 | from ming import schema |
11 | 12 | from ming.orm import FieldProperty, ForeignIdProperty, Mapper, session, state |
13 | +from ming.orm.declarative import MappedClass | |
14 | + | |
12 | 15 | from allura import model as M |
13 | 16 | from allura.lib import helpers as h |
14 | -from allura.lib import utils, patience | |
17 | +from allura.lib import utils | |
15 | 18 | |
16 | 19 | config = utils.ConfigProxy( |
17 | 20 | common_suffix='forgemail.domain') |
18 | 21 | |
22 | +class Globals(MappedClass): | |
23 | + | |
24 | + class __mongometa__: | |
25 | + name = 'blog-globals' | |
26 | + session = M.project_orm_session | |
27 | + indexes = [ 'app_config_id' ] | |
28 | + | |
29 | + type_s = 'BlogGlobals' | |
30 | + _id = FieldProperty(schema.ObjectId) | |
31 | + app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id) | |
32 | + external_feeds=FieldProperty([str]) | |
33 | + | |
34 | + | |
19 | 35 | class BlogPostSnapshot(M.Snapshot): |
20 | 36 | class __mongometa__: |
21 | 37 | name='blog_post_snapshot' |
@@ -166,7 +182,7 @@ class BlogPost(M.VersionedArtifact): | ||
166 | 182 | v2 = self |
167 | 183 | la = [ line + '\n' for line in v1.text.splitlines() ] |
168 | 184 | lb = [ line + '\n' for line in v2.text.splitlines() ] |
169 | - diff = ''.join(patience.unified_diff( | |
185 | + diff = ''.join(difflib.unified_diff( | |
170 | 186 | la, lb, |
171 | 187 | 'v%d' % v1.version, |
172 | 188 | 'v%d' % v2.version)) |
@@ -0,0 +1,27 @@ | ||
1 | +<form method="POST" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/set_exfeed"> | |
2 | + <label class="grid-13">Existing external feeds:</label> | |
3 | + <div class="grid-13"> | |
4 | + <ul> | |
5 | + {% if allow_config %} | |
6 | + {% for feed in feeds_list %} | |
7 | + <li><input type="checkbox" name="exfeed" value="{{ feed }}" checked="checked"><span>{{ feed }}</span></li> | |
8 | + {% endfor %} | |
9 | + {% else %} | |
10 | + {% for feed in feeds_list %} | |
11 | + <li><span>{{ feed.value }}</span></li> | |
12 | + {% endfor %} | |
13 | + {% endif %} | |
14 | + </div> | |
15 | + {% if allow_config %} | |
16 | + <div class="grid-13"> </div> | |
17 | + <div class="grid-13"> | |
18 | + <input type="text" name="new_exfeed" id="new_exfeed" value=""/> | |
19 | + </div> | |
20 | + <div class="grid-13"> </div> | |
21 | + <hr> | |
22 | + <div class="grid-13"> </div> | |
23 | + <div class="grid-13"> | |
24 | + <input type="submit" value="Save"/> | |
25 | + </div> | |
26 | + {% endif %} | |
27 | +</form> |
@@ -56,6 +56,7 @@ class TestRootController(TestController): | ||
56 | 56 | |
57 | 57 | def test_root_new_post(self): |
58 | 58 | response = self.app.get('/blog/new') |
59 | + assert '<option selected value="published">Published</option>' in response | |
59 | 60 | assert 'Enter your title here' in response |
60 | 61 | |
61 | 62 | def test_validation(self): |
@@ -37,18 +37,26 @@ class TestRootController(TestController): | ||
37 | 37 | ThreadLocalORMSession.close_all() |
38 | 38 | |
39 | 39 | def test_fork(self): |
40 | + r = self.app.get('%sfork/' % c.app.repo.url()) | |
41 | + assert '<input type="text" name="mount_point" value="test"/>' in r | |
42 | + assert '<input type="text" name="mount_label" value="test - Git"/>' in r | |
43 | + | |
40 | 44 | to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id) |
45 | + mount_point = 'reponame' | |
41 | 46 | r = self.app.post('/src-git/fork', params=dict( |
42 | 47 | project_id=str(to_project._id), |
43 | - to_name='code')) | |
48 | + mount_point=mount_point, | |
49 | + mount_label='Test forked repository')) | |
50 | + assert "{status: 'error'}" not in str(r.follow()) | |
44 | 51 | cloned_from = c.app.repo |
45 | - with h.push_context('test2', 'code', neighborhood='Projects'): | |
52 | + with h.push_context('test2', mount_point, neighborhood='Projects'): | |
46 | 53 | c.app.repo.init_as_clone( |
47 | 54 | cloned_from.full_fs_path, |
48 | 55 | cloned_from.app.config.script_name(), |
49 | 56 | cloned_from.full_fs_path) |
50 | - r = self.app.get('/p/test2/code').follow().follow().follow() | |
57 | + r = self.app.get('/p/test2/%s' % mount_point).follow().follow().follow() | |
51 | 58 | assert 'Clone of' in r |
59 | + assert 'Test forked repository' in r | |
52 | 60 | r = self.app.get('/src-git/').follow().follow() |
53 | 61 | assert 'Forks' in r |
54 | 62 |
@@ -56,7 +64,7 @@ class TestRootController(TestController): | ||
56 | 64 | to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id) |
57 | 65 | r = self.app.post('/src-git/fork', params=dict( |
58 | 66 | project_id=str(to_project._id), |
59 | - to_name='code')) | |
67 | + mount_point='code')) | |
60 | 68 | cloned_from = c.app.repo |
61 | 69 | with h.push_context('test2', 'code', neighborhood='Projects'): |
62 | 70 | c.app.repo.init_as_clone( |
@@ -147,8 +155,8 @@ class TestRootController(TestController): | ||
147 | 155 | def test_file(self): |
148 | 156 | ci = self._get_ci() |
149 | 157 | resp = self.app.get(ci + 'tree/README') |
150 | - assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2] | |
151 | - content = str(resp.html.find('div',{'class':'clip grid-19'})) | |
158 | + assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2] | |
159 | + content = str(resp.html.find('div', {'class':'clip grid-19'})) | |
152 | 160 | assert 'This is readme' in content, content |
153 | 161 | assert '<span id="l1" class="code_block">' in resp |
154 | 162 | assert 'var hash = window.location.hash.substring(1);' in resp |
@@ -174,6 +182,6 @@ class TestRootController(TestController): | ||
174 | 182 | def test_file_force_display(self): |
175 | 183 | ci = self._get_ci() |
176 | 184 | resp = self.app.get(ci + 'tree/README?force=True') |
177 | - content = str(resp.html.find('div',{'class':'clip grid-19'})) | |
185 | + content = str(resp.html.find('div', {'class':'clip grid-19'})) | |
178 | 186 | assert re.search(r'<pre>.*This is readme', content), content |
179 | 187 | assert '</pre>' in content, content |
@@ -1,6 +1,9 @@ | ||
1 | 1 | import json |
2 | 2 | |
3 | 3 | import pkg_resources |
4 | +import pylons | |
5 | +pylons.c = pylons.tmpl_context | |
6 | +pylons.g = pylons.app_globals | |
4 | 7 | from pylons import c |
5 | 8 | from ming.orm import ThreadLocalORMSession |
6 | 9 | from datadiff.tools import assert_equal |
@@ -35,7 +38,8 @@ class TestRootController(TestController): | ||
35 | 38 | to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id) |
36 | 39 | r = self.app.post('/src-hg/fork', params=dict( |
37 | 40 | project_id=str(to_project._id), |
38 | - to_name='code')) | |
41 | + mount_point='code')) | |
42 | + assert "{status: 'error'}" not in str(r.follow()) | |
39 | 43 | cloned_from = c.app.repo |
40 | 44 | with h.push_context('test2', 'code', neighborhood='Projects'): |
41 | 45 | c.app.repo.init_as_clone( |
@@ -51,7 +55,8 @@ class TestRootController(TestController): | ||
51 | 55 | to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id) |
52 | 56 | r = self.app.post('/src-hg/fork', params=dict( |
53 | 57 | project_id=str(to_project._id), |
54 | - to_name='code')) | |
58 | + mount_point='code')) | |
59 | + assert "{status: 'error'}" not in str(r.follow()) | |
55 | 60 | cloned_from = c.app.repo |
56 | 61 | with h.push_context('test2', 'code', neighborhood='Projects'): |
57 | 62 | c.app.repo.init_as_clone( |
@@ -117,14 +122,14 @@ class TestRootController(TestController): | ||
117 | 122 | def test_tree(self): |
118 | 123 | ci = self._get_ci() |
119 | 124 | resp = self.app.get(ci + 'tree/') |
120 | - assert len(resp.html.findAll('tr')) ==2, resp.showbrowser() | |
125 | + assert len(resp.html.findAll('tr')) == 2, resp.showbrowser() | |
121 | 126 | assert 'README' in resp, resp.showbrowser() |
122 | 127 | |
123 | 128 | def test_file(self): |
124 | 129 | ci = self._get_ci() |
125 | 130 | resp = self.app.get(ci + 'tree/README') |
126 | - assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2] | |
127 | - content = str(resp.html.find('div',{'class':'clip grid-19'})) | |
131 | + assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2] | |
132 | + content = str(resp.html.find('div', {'class':'clip grid-19'})) | |
128 | 133 | assert 'This is readme' in content, content |
129 | 134 | assert '<span id="l1" class="code_block">' in resp |
130 | 135 | assert 'var hash = window.location.hash.substring(1);' in resp |
@@ -259,12 +259,15 @@ class SVNImplementation(M.RepositoryImplementation): | ||
259 | 259 | log.info('ClientError processing %r %r, treating as empty', ci, self._repo, exc_info=True) |
260 | 260 | log_entry = Object(date=0, message='', changed_paths=[]) |
261 | 261 | # Save commit metadata |
262 | + log_date = None | |
263 | + if hasattr(log_entry, 'date'): | |
264 | + log_date = datetime.utcfromtimestamp(log_entry.date) | |
262 | 265 | ci.committed = Object( |
263 | 266 | name=log_entry.get('author', '--none--'), |
264 | 267 | email='', |
265 | - date=datetime.utcfromtimestamp(log_entry.date)) | |
268 | + date=log_date) | |
266 | 269 | ci.authored=Object(ci.committed) |
267 | - ci.message=log_entry.message | |
270 | + ci.message=log_entry.get("message", "--none--") | |
268 | 271 | if revno > 1: |
269 | 272 | parent_oid = self._oid(revno - 1) |
270 | 273 | ci.parent_ids = [ parent_oid ] |
@@ -278,13 +281,14 @@ class SVNImplementation(M.RepositoryImplementation): | ||
278 | 281 | D=ci.diffs.removed, |
279 | 282 | M=ci.diffs.changed, |
280 | 283 | R=ci.diffs.changed) |
281 | - for path in log_entry.changed_paths: | |
282 | - if path.copyfrom_path: | |
283 | - ci.diffs.copied.append(dict( | |
284 | - old=h.really_unicode(path.copyfrom_path), | |
285 | - new=h.really_unicode(path.path))) | |
286 | - continue | |
287 | - lst[path.action].append(h.really_unicode(path.path)) | |
284 | + if hasattr(log_entry, 'changed_paths'): | |
285 | + for path in log_entry.changed_paths: | |
286 | + if path.copyfrom_path: | |
287 | + ci.diffs.copied.append(dict( | |
288 | + old=h.really_unicode(path.copyfrom_path), | |
289 | + new=h.really_unicode(path.path))) | |
290 | + continue | |
291 | + lst[path.action].append(h.really_unicode(path.path)) | |
288 | 292 | |
289 | 293 | def refresh_commit_info(self, oid, seen_object_ids, lazy=True): |
290 | 294 | from allura.model.repo import CommitDoc |
@@ -301,15 +305,18 @@ class SVNImplementation(M.RepositoryImplementation): | ||
301 | 305 | except pysvn.ClientError: |
302 | 306 | log.info('ClientError processing %r %r, treating as empty', oid, self._repo, exc_info=True) |
303 | 307 | log_entry = Object(date='', message='', changed_paths=[]) |
308 | + log_date = None | |
309 | + if hasattr(log_entry, 'date'): | |
310 | + log_date = datetime.utcfromtimestamp(log_entry.date) | |
304 | 311 | user = Object( |
305 | 312 | name=log_entry.get('author', '--none--'), |
306 | 313 | email='', |
307 | - date=datetime.utcfromtimestamp(log_entry.date)) | |
314 | + date=log_date) | |
308 | 315 | args = dict( |
309 | 316 | tree_id=None, |
310 | 317 | committed=user, |
311 | 318 | authored=user, |
312 | - message=log_entry.message, | |
319 | + message=log_entry.get("message", "--none--"), | |
313 | 320 | parent_ids=[], |
314 | 321 | child_ids=[]) |
315 | 322 | if revno > 1: |
@@ -73,8 +73,8 @@ class TestRootController(TestController): | ||
73 | 73 | |
74 | 74 | def test_file(self): |
75 | 75 | resp = self.app.get('/src/1/tree/README') |
76 | - assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2] | |
77 | - content = str(resp.html.find('div',{'class':'clip grid-19'})) | |
76 | + assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2] | |
77 | + content = str(resp.html.find('div', {'class':'clip grid-19'})) | |
78 | 78 | assert 'This is readme' in content, content |
79 | 79 | assert '<span id="l1" class="code_block">' in resp |
80 | 80 | assert 'var hash = window.location.hash.substring(1);' in resp |
@@ -1,4 +1,4 @@ | ||
1 | -{% python from allura.lib import patience %}\ | |
1 | +{% python import difflib %}\ | |
2 | 2 | {% python from allura.model import User %}\ |
3 | 3 | {% for key, values in changelist %}\ |
4 | 4 | {% choose %}\ |
@@ -9,7 +9,7 @@ Diff: | ||
9 | 9 | |
10 | 10 | ~~~~ |
11 | 11 | |
12 | -${'\n'.join(patience.unified_diff( | |
12 | +${'\n'.join(difflib.unified_diff( | |
13 | 13 | a=values[0].splitlines(), |
14 | 14 | b=values[1].splitlines(), |
15 | 15 | fromfile='old', |
@@ -1,6 +1,7 @@ | ||
1 | 1 | import logging |
2 | 2 | import urllib |
3 | 3 | import json |
4 | +import difflib | |
4 | 5 | from datetime import datetime, timedelta |
5 | 6 | |
6 | 7 | import bson |
@@ -20,7 +21,6 @@ from ming.orm.declarative import MappedClass | ||
20 | 21 | from allura.model import Artifact, VersionedArtifact, Snapshot, project_orm_session, BaseAttachment |
21 | 22 | from allura.model import User, Feed, Thread, Notification, ProjectRole |
22 | 23 | from allura.model import ACE, ALL_PERMISSIONS, DENY_ALL |
23 | -from allura.lib import patience | |
24 | 24 | from allura.lib import security |
25 | 25 | from allura.lib.search import search_artifact |
26 | 26 | from allura.lib import utils |
@@ -392,7 +392,7 @@ class Ticket(VersionedArtifact): | ||
392 | 392 | if old.description != self.description: |
393 | 393 | changes.append('Description updated:') |
394 | 394 | changes.append('\n'.join( |
395 | - patience.unified_diff( | |
395 | + difflib.unified_diff( | |
396 | 396 | a=old.description.split('\n'), |
397 | 397 | b=self.description.split('\n'), |
398 | 398 | fromfile='description-old', |
@@ -1241,8 +1241,9 @@ class TrackerAdminController(DefaultAdminController): | ||
1241 | 1241 | self.app.globals.closed_status_names=post_data['closed_status_names'] |
1242 | 1242 | custom_fields = post_data.get('custom_fields', []) |
1243 | 1243 | for field in custom_fields: |
1244 | - field['name'] = '_' + '_'.join([ | |
1245 | - w for w in NONALNUM_RE.split(field['label'].lower()) if w]) | |
1244 | + if 'name' not in field or not field['name']: | |
1245 | + field['name'] = '_' + '_'.join([ | |
1246 | + w for w in NONALNUM_RE.split(field['label'].lower()) if w]) | |
1246 | 1247 | if field['type'] == 'milestone': |
1247 | 1248 | field.setdefault('milestones', []) |
1248 | 1249 |
@@ -81,6 +81,7 @@ class CustomFieldAdmin(ew.CompoundField): | ||
81 | 81 | yield ew.JSLink('tracker_js/custom-fields.js') |
82 | 82 | |
83 | 83 | fields = [ |
84 | + ew.HiddenField(name='name'), | |
84 | 85 | ew.TextField(name='label'), |
85 | 86 | ew.Checkbox( |
86 | 87 | name='show_in_search', |
@@ -1,4 +1,5 @@ | ||
1 | 1 | import pylons |
2 | +import difflib | |
2 | 3 | pylons.c = pylons.tmpl_context |
3 | 4 | pylons.g = pylons.app_globals |
4 | 5 | from pylons import g #g is a namespace for globally accessable app helpers |
@@ -11,7 +12,6 @@ from ming.orm.declarative import MappedClass | ||
11 | 12 | from allura.model import VersionedArtifact, Snapshot, Feed, Thread, Post, User, BaseAttachment |
12 | 13 | from allura.model import Notification, project_orm_session |
13 | 14 | from allura.lib import helpers as h |
14 | -from allura.lib import patience | |
15 | 15 | from allura.lib import utils |
16 | 16 | |
17 | 17 | config = utils.ConfigProxy( |
@@ -86,7 +86,7 @@ class Page(VersionedArtifact): | ||
86 | 86 | v2 = self |
87 | 87 | la = [ line + '\n' for line in v1.text.splitlines() ] |
88 | 88 | lb = [ line + '\n' for line in v2.text.splitlines() ] |
89 | - diff = ''.join(patience.unified_diff( | |
89 | + diff = ''.join(difflib.unified_diff( | |
90 | 90 | la, lb, |
91 | 91 | 'v%d' % v1.version, |
92 | 92 | 'v%d' % v2.version)) |
@@ -15,6 +15,7 @@ FormEncode==1.2.4 | ||
15 | 15 | # dep of Creoleparser |
16 | 16 | Genshi==0.6 |
17 | 17 | # dep of oauth2 |
18 | +html2text==3.200.3 | |
18 | 19 | httplib2==0.7.4 |
19 | 20 | iso8601==0.1.4 |
20 | 21 | Jinja2==2.6 |
@@ -39,6 +40,7 @@ pytidylib==0.2.1 | ||
39 | 40 | textile==2.1.5 |
40 | 41 | # dep of colander |
41 | 42 | translationstring==0.4 |
43 | +TimerMiddleware==0.2.1 | |
42 | 44 | TurboGears2==2.1.3 |
43 | 45 | # part of the stdlib, but with a version number. see http://guide.python-distribute.org/pip.html#listing-installed-packages |
44 | 46 | wsgiref==0.1.2 |
@@ -14,7 +14,6 @@ sqlalchemy-migrate==0.7.1 | ||
14 | 14 | pyzmq==2.1.7 |
15 | 15 | |
16 | 16 | # for the migration scripts only |
17 | -html2text==3.101 | |
18 | 17 | postmarkup==1.1.4 |
19 | 18 | # suds needed for teamforge import script |
20 | 19 | suds==0.4 |
@@ -7,6 +7,7 @@ from ming.orm import session | ||
7 | 7 | from bson import ObjectId |
8 | 8 | |
9 | 9 | from allura import model as M |
10 | +from allura.lib import utils | |
10 | 11 | from forgewiki.wiki_main import ForgeWikiApp |
11 | 12 | |
12 | 13 | log = logging.getLogger('fix-home-permissions') |
@@ -22,8 +23,9 @@ def main(): | ||
22 | 23 | else: |
23 | 24 | log.info('Fixing permissions for all Home Wikis') |
24 | 25 | |
25 | - for some_projects in chunked_project_iterator({'neighborhood_id': {'$nin': [ObjectId('4be2faf8898e33156f00003e'), # /u | |
26 | - ObjectId('4dbf2563bfc09e6362000005')]}}): # /motorola | |
26 | + for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': { | |
27 | + '$nin': [ObjectId('4be2faf8898e33156f00003e'), # /u | |
28 | + ObjectId('4dbf2563bfc09e6362000005')]}}): # /motorola | |
27 | 29 | for project in some_projects: |
28 | 30 | c.project = project |
29 | 31 | home_app = project.app_instance('home') |
@@ -56,21 +58,6 @@ def main(): | ||
56 | 58 | home_app.config.acl = map(dict, new_acl.values()) |
57 | 59 | session(home_app.config).flush() |
58 | 60 | |
59 | -PAGESIZE=1024 | |
60 | - | |
61 | -def chunked_project_iterator(q_project): | |
62 | - '''shamelessly copied from refresh-all-repos.py''' | |
63 | - page = 0 | |
64 | - while True: | |
65 | - results = (M.Project.query | |
66 | - .find(q_project) | |
67 | - .skip(PAGESIZE*page) | |
68 | - .limit(PAGESIZE) | |
69 | - .all()) | |
70 | - if not results: break | |
71 | - yield results | |
72 | - page += 1 | |
73 | - | |
74 | 61 | def project_role(project, name): |
75 | 62 | role = M.ProjectRole.query.get(project_id=project._id, name=name) |
76 | 63 | if role is None: |
@@ -12,11 +12,11 @@ For project.users: | ||
12 | 12 | import sys |
13 | 13 | import logging |
14 | 14 | |
15 | -from pylons import c | |
16 | 15 | from ming.orm import session |
17 | 16 | from ming.orm.ormsession import ThreadLocalORMSession |
18 | 17 | |
19 | 18 | from allura import model as M |
19 | +from allura.lib import utils | |
20 | 20 | |
21 | 21 | log = logging.getLogger('fix-subroles') |
22 | 22 | log.addHandler(logging.StreamHandler(sys.stdout)) |
@@ -27,7 +27,7 @@ def main(): | ||
27 | 27 | log.info('Examining subroles in all non-user projects.') |
28 | 28 | n_users = M.Neighborhood.query.get(name='Users') |
29 | 29 | project_filter = dict(neighborhood_id={'$ne':n_users._id}) |
30 | - for some_projects in chunked_project_iterator(project_filter): | |
30 | + for some_projects in utils.chunked_find(M.Project, project_filter): | |
31 | 31 | for project in some_projects: |
32 | 32 | project_name = '%s.%s' % (project.neighborhood.name, project.shortname) |
33 | 33 | project_roles = {} |
@@ -53,7 +53,7 @@ def main(): | ||
53 | 53 | for user in project.users(): |
54 | 54 | pr = user.project_role(project=project) |
55 | 55 | if not pr.roles: continue |
56 | - for parent, children in [('Admin', ('Developer', 'Member')), | |
56 | + for parent, children in [('Admin', ('Developer', 'Member')), | |
57 | 57 | ('Developer', ('Member',))]: |
58 | 58 | if project_roles[parent]._id not in pr.roles: continue |
59 | 59 | for role_name in children: |
@@ -73,21 +73,5 @@ def main(): | ||
73 | 73 | |
74 | 74 | log.info('%s projects examined.' % num_projects_examined) |
75 | 75 | |
76 | - | |
77 | -PAGESIZE=1024 | |
78 | - | |
79 | -def chunked_project_iterator(q_project): | |
80 | - '''shamelessly copied from refresh-all-repos.py''' | |
81 | - page = 0 | |
82 | - while True: | |
83 | - results = (M.Project.query | |
84 | - .find(q_project) | |
85 | - .skip(PAGESIZE*page) | |
86 | - .limit(PAGESIZE) | |
87 | - .all()) | |
88 | - if not results: break | |
89 | - yield results | |
90 | - page += 1 | |
91 | - | |
92 | 76 | if __name__ == '__main__': |
93 | 77 | main() |
@@ -7,6 +7,7 @@ from bson import ObjectId | ||
7 | 7 | from mock import Mock, patch |
8 | 8 | |
9 | 9 | from allura.lib import helpers as h |
10 | +from allura.lib import utils | |
10 | 11 | from allura import model as M |
11 | 12 | from forgewiki import model as WM |
12 | 13 | from allura.ext.project_home import ProjectHomeApp |
@@ -21,7 +22,8 @@ def main(): | ||
21 | 22 | possibly_orphaned_projects = 0 |
22 | 23 | solr_delete = Mock() |
23 | 24 | notification_post = Mock() |
24 | - for some_projects in chunked_project_iterator({'neighborhood_id': {'$ne': ObjectId("4be2faf8898e33156f00003e")}}): | |
25 | + for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': { | |
26 | + '$ne': ObjectId("4be2faf8898e33156f00003e")}}): | |
25 | 27 | for project in some_projects: |
26 | 28 | c.project = project |
27 | 29 | old_home_app = project.app_instance('home') |
@@ -102,20 +104,5 @@ def main(): | ||
102 | 104 | assert solr_delete.call_count == affected_projects, solr_delete.call_count |
103 | 105 | assert notification_post.call_count == 2 * affected_projects, notification_post.call_count |
104 | 106 | |
105 | -PAGESIZE=1024 | |
106 | - | |
107 | -def chunked_project_iterator(q_project): | |
108 | - '''shamelessly copied from refresh-all-repos.py''' | |
109 | - page = 0 | |
110 | - while True: | |
111 | - results = (M.Project.query | |
112 | - .find(q_project) | |
113 | - .skip(PAGESIZE*page) | |
114 | - .limit(PAGESIZE) | |
115 | - .all()) | |
116 | - if not results: break | |
117 | - yield results | |
118 | - page += 1 | |
119 | - | |
120 | 107 | if __name__ == '__main__': |
121 | 108 | main() |
@@ -6,6 +6,7 @@ from ming.orm import session | ||
6 | 6 | from ming.orm.ormsession import ThreadLocalORMSession |
7 | 7 | |
8 | 8 | from allura import model as M |
9 | +from allura.lib import utils | |
9 | 10 | |
10 | 11 | log = logging.getLogger('update-ordinals') |
11 | 12 | log.addHandler(logging.StreamHandler(sys.stdout)) |
@@ -14,7 +15,7 @@ def main(): | ||
14 | 15 | test = sys.argv[-1] == 'test' |
15 | 16 | num_projects_examined = 0 |
16 | 17 | log.info('Examining all projects for mount order.') |
17 | - for some_projects in chunked_project_iterator({}): | |
18 | + for some_projects in utils.chunked_find(M.Project): | |
18 | 19 | for project in some_projects: |
19 | 20 | c.project = project |
20 | 21 | mounts = project.ordered_mounts(include_search=True) |
@@ -47,21 +48,5 @@ def main(): | ||
47 | 48 | ThreadLocalORMSession.flush_all() |
48 | 49 | ThreadLocalORMSession.close_all() |
49 | 50 | |
50 | - | |
51 | -PAGESIZE=1024 | |
52 | - | |
53 | -def chunked_project_iterator(q_project): | |
54 | - '''shamelessly copied from refresh-all-repos.py''' | |
55 | - page = 0 | |
56 | - while True: | |
57 | - results = (M.Project.query | |
58 | - .find(q_project) | |
59 | - .skip(PAGESIZE*page) | |
60 | - .limit(PAGESIZE) | |
61 | - .all()) | |
62 | - if not results: break | |
63 | - yield results | |
64 | - page += 1 | |
65 | - | |
66 | 51 | if __name__ == '__main__': |
67 | 52 | main() |
@@ -1,56 +0,0 @@ | ||
1 | -import logging | |
2 | -import re | |
3 | - | |
4 | -from pylons import c | |
5 | - | |
6 | -from ming.orm import ThreadLocalORMSession | |
7 | - | |
8 | -from allura import model as M | |
9 | -from forgewiki import model as WM | |
10 | -from forgewiki.wiki_main import ForgeWikiApp | |
11 | - | |
12 | -log = logging.getLogger(__name__) | |
13 | - | |
14 | -default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$' | |
15 | - | |
16 | -default_personal_project_tmpl = ("This is the personal project of %s." | |
17 | - " This project is created automatically during user registration" | |
18 | - " as an easy place to store personal data that doesn't need its own" | |
19 | - " project such as cloned repositories.\n\n%s") | |
20 | - | |
21 | -def main(): | |
22 | - for p in M.Project.query.find().all(): | |
23 | - user = p.user_project_of | |
24 | - if not user: | |
25 | - continue | |
26 | - | |
27 | - description = p.description | |
28 | - if description is None or re.match(default_description, description): | |
29 | - continue | |
30 | - | |
31 | - app = p.app_instance('wiki') | |
32 | - if app is None: | |
33 | - p.install_app('wiki') | |
34 | - | |
35 | - page = WM.Page.query.get(app_config_id=app.config._id, title='Home') | |
36 | - if page is None: | |
37 | - continue | |
38 | - | |
39 | - c.app = app | |
40 | - c.project = p | |
41 | - c.user = user | |
42 | - | |
43 | - if "This is the personal project of" in page.text: | |
44 | - if description not in page.text: | |
45 | - page.text = "%s\n\n%s" % (page.text, description) | |
46 | - log.info("Update wiki home page text for %s" % user.username) | |
47 | - elif "This is the default page" in page.text: | |
48 | - page.text = default_personal_project_tmpl % (user.display_name, description) | |
49 | - log.info("Update wiki home page text for %s" % user.username) | |
50 | - else: | |
51 | - pass | |
52 | - | |
53 | - ThreadLocalORMSession.flush_all() | |
54 | - | |
55 | -if __name__ == '__main__': | |
56 | - main() |
@@ -0,0 +1,59 @@ | ||
1 | +import logging | |
2 | +import re | |
3 | + | |
4 | +from pylons import c | |
5 | + | |
6 | +from ming.orm import ThreadLocalORMSession | |
7 | + | |
8 | +from allura import model as M | |
9 | +from allura.lib import utils | |
10 | +from forgewiki import model as WM | |
11 | +from forgewiki.wiki_main import ForgeWikiApp | |
12 | + | |
13 | +log = logging.getLogger(__name__) | |
14 | + | |
15 | +default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$' | |
16 | + | |
17 | +default_personal_project_tmpl = ("This is the personal project of %s." | |
18 | + " This project is created automatically during user registration" | |
19 | + " as an easy place to store personal data that doesn't need its own" | |
20 | + " project such as cloned repositories.\n\n%s") | |
21 | + | |
22 | +def main(): | |
23 | + users = M.Neighborhood.query.get(name='Users') | |
24 | + for chunk in utils.chunked_find(M.Project, {'neighborhood_id': users._id}): | |
25 | + for p in chunk: | |
26 | + user = p.user_project_of | |
27 | + if not user: | |
28 | + continue | |
29 | + | |
30 | + description = p.description | |
31 | + if description is None or re.match(default_description, description): | |
32 | + continue | |
33 | + | |
34 | + app = p.app_instance('wiki') | |
35 | + if app is None: | |
36 | + p.install_app('wiki') | |
37 | + | |
38 | + page = WM.Page.query.get(app_config_id=app.config._id, title='Home') | |
39 | + if page is None: | |
40 | + continue | |
41 | + | |
42 | + c.app = app | |
43 | + c.project = p | |
44 | + c.user = user | |
45 | + | |
46 | + if "This is the personal project of" in page.text: | |
47 | + if description not in page.text: | |
48 | + page.text = "%s\n\n%s" % (page.text, description) | |
49 | + log.info("Update wiki home page text for %s" % user.username) | |
50 | + elif "This is the default page" in page.text: | |
51 | + page.text = default_personal_project_tmpl % (user.display_name, description) | |
52 | + log.info("Update wiki home page text for %s" % user.username) | |
53 | + else: | |
54 | + pass | |
55 | + | |
56 | + ThreadLocalORMSession.flush_all() | |
57 | + | |
58 | +if __name__ == '__main__': | |
59 | + main() |
@@ -1,11 +1,11 @@ | ||
1 | 1 | import logging |
2 | 2 | import optparse |
3 | -from collections import defaultdict | |
4 | 3 | |
5 | 4 | from pylons import c |
6 | 5 | from ming.orm import ThreadLocalORMSession |
7 | 6 | |
8 | 7 | from allura import model as M |
8 | +from allura.lib import utils | |
9 | 9 | |
10 | 10 | log = logging.getLogger(__name__) |
11 | 11 |
@@ -45,7 +45,7 @@ def main(): | ||
45 | 45 | M.repo.DiffInfoDoc.m.remove({}) |
46 | 46 | M.repo.LastCommitDoc.m.remove({}) |
47 | 47 | M.repo.CommitRunDoc.m.remove({}) |
48 | - for chunk in chunked_project_iterator(q_project): | |
48 | + for chunk in utils.chunked_find(M.Project, q_project): | |
49 | 49 | for p in chunk: |
50 | 50 | c.project = p |
51 | 51 | if projects: |
@@ -73,18 +73,5 @@ def main(): | ||
73 | 73 | ThreadLocalORMSession.flush_all() |
74 | 74 | ThreadLocalORMSession.close_all() |
75 | 75 | |
76 | -def chunked_project_iterator(q_project): | |
77 | - page = 0 | |
78 | - while True: | |
79 | - results = (M.Project.query | |
80 | - .find(q_project) | |
81 | - .skip(PAGESIZE*page) | |
82 | - .limit(PAGESIZE) | |
83 | - .all()) | |
84 | - if not results: break | |
85 | - yield results | |
86 | - page += 1 | |
87 | - | |
88 | - | |
89 | 76 | if __name__ == '__main__': |
90 | 77 | main() |