allura
Revision | 0891930e3979da21ed0c27ea03ed877ce8559e43 (tree) |
---|---|
Time | 2012-03-22 04:33:16 |
Author | Yaroslav Luzin <jardev@gmai...> |
Commiter | Yaroslav Luzin |
Merge branch 'master' into t27_donotshowcss
@@ -1,8 +1,8 @@ | ||
1 | -from . import base | |
1 | +from allura.command import base | |
2 | 2 | |
3 | 3 | from bson import ObjectId |
4 | 4 | from allura import model as M |
5 | -from allura.lib import plugin | |
5 | +from allura.lib import plugin, exceptions | |
6 | 6 | from ming.orm import session |
7 | 7 | |
8 | 8 |
@@ -12,8 +12,9 @@ class SetNeighborhoodLevelCommand(base.Command): | ||
12 | 12 | min_args = 3 |
13 | 13 | max_args = 3 |
14 | 14 | usage = "<ini file> <neighborhood> <level>" # not sure if we need ini file |
15 | - summary = "Change neighborhood level. <neighgborhood> - " \ | |
16 | - "should neightborhood's name or id" | |
15 | + summary = "Change the neighborhood level\r\n" \ | |
16 | + "\t<neighgborhood> - the neighborhood name or object id\r\n" \ | |
17 | + "\t<level> - silver, gold or platinum" | |
17 | 18 | parser = base.Command.standard_parser(verbose=True) |
18 | 19 | |
19 | 20 | def command(self): |
@@ -21,15 +22,17 @@ class SetNeighborhoodLevelCommand(base.Command): | ||
21 | 22 | n_id = self.args[1] |
22 | 23 | n_level = self.args[2] |
23 | 24 | if n_level not in ["silver", "gold", "platinum"]: |
24 | - base.log.error("You must select one of three level types (silver, gold, or platinum)") | |
25 | - return | |
25 | + raise exceptions.NoSuchNBLevelError("%s is not a valid " \ | |
26 | + "neighborhood level. The valid levels are \"silver\", " \ | |
27 | + "\"gold\" and \"platinum\"" % n_level) | |
26 | 28 | |
27 | 29 | n = M.Neighborhood.query.get(name=n_id) |
28 | 30 | if not n: |
29 | 31 | n = M.Neighborhood.query.get(_id=ObjectId(n_id)) |
30 | 32 | |
31 | 33 | if not n: |
32 | - base.log.error("The neighborhood % scould not be found" % n_id) | |
34 | + raise exceptions.NoSuchNeighborhoodError("The neighborhood %s " \ | |
35 | + "could not be found in the database" % n_id) | |
33 | 36 | else: |
34 | 37 | n.level = n_level |
35 | 38 | if n_level == "gold": |
@@ -87,10 +87,10 @@ def _make_core_app(root, global_conf, full_stack=True, **app_conf): | ||
87 | 87 | |
88 | 88 | if config.get('zarkov.host'): |
89 | 89 | try: |
90 | - from zarkov import client as zclient | |
91 | - except ImportError, e: | |
92 | - raise ImportError, "Unable to import the zarkov library. Please"\ | |
93 | - " check that zarkov is installed or comment out"\ | |
90 | + import zmq | |
91 | + except ImportError: | |
92 | + raise ImportError, "Unable to import the zmq library. Please"\ | |
93 | + " check that zeromq is installed or comment out"\ | |
94 | 94 | " the zarkov.host setting in your ini file." |
95 | 95 | |
96 | 96 | app = tg.TGApp() |
@@ -384,6 +384,8 @@ class CommitBrowser(BaseController): | ||
384 | 384 | result = dict(commit=self._commit) |
385 | 385 | if self._commit: |
386 | 386 | result.update(self._commit.context()) |
387 | + result['artifacts'] = [(t,f) for t in ('added', 'removed', 'changed', 'copied') | |
388 | + for f in self._commit.diffs[t]] | |
387 | 389 | return result |
388 | 390 | |
389 | 391 | @expose('jinja:allura:templates/repo/commit_basic.html') |
@@ -473,6 +475,9 @@ class FileBrowser(BaseController): | ||
473 | 475 | elif 'diff' in kw: |
474 | 476 | tg.decorators.override_template(self.index, 'jinja:allura:templates/repo/diff.html') |
475 | 477 | return self.diff(kw['diff']) |
478 | + elif 'barediff' in kw: | |
479 | + tg.decorators.override_template(self.index, 'jinja:allura:templates/repo/barediff.html') | |
480 | + return self.diff(kw['barediff']) | |
476 | 481 | else: |
477 | 482 | force_display = 'force' in kw |
478 | 483 | context = self._blob.context() |
@@ -4,14 +4,11 @@ | ||
4 | 4 | |
5 | 5 | __all__ = ['Globals'] |
6 | 6 | import logging |
7 | -import socket | |
8 | 7 | import cgi |
9 | 8 | import json |
10 | 9 | import shlex |
11 | 10 | import datetime |
12 | 11 | from urllib import urlencode |
13 | -from ConfigParser import RawConfigParser | |
14 | -from collections import defaultdict | |
15 | 12 | |
16 | 13 | import pkg_resources |
17 | 14 |
@@ -21,7 +18,6 @@ import pygments | ||
21 | 18 | import pygments.lexers |
22 | 19 | import pygments.formatters |
23 | 20 | import pygments.util |
24 | -import webob.exc | |
25 | 21 | from tg import config, session |
26 | 22 | from pylons import c, request |
27 | 23 | from paste.deploy.converters import asbool, asint |
@@ -31,11 +27,6 @@ import ew as ew_core | ||
31 | 27 | import ew.jinja2_ew as ew |
32 | 28 | from ming.utils import LazyProperty |
33 | 29 | |
34 | -try: | |
35 | - from zarkov import client as zclient | |
36 | -except ImportError: | |
37 | - zclient = None | |
38 | - | |
39 | 30 | import allura.tasks.event_tasks |
40 | 31 | from allura import model as M |
41 | 32 | from allura.lib.markdown_extensions import ForgeExtension |
@@ -45,6 +36,7 @@ from allura.lib import helpers as h | ||
45 | 36 | from allura.lib.widgets import analytics |
46 | 37 | from allura.lib.security import Credentials |
47 | 38 | from allura.lib.async import Connection, MockAMQ |
39 | +from allura.lib.zarkov_helpers import ZarkovClient, zmq | |
48 | 40 | |
49 | 41 | log = logging.getLogger(__name__) |
50 | 42 |
@@ -173,7 +165,7 @@ class Globals(object): | ||
173 | 165 | mount_point=None, |
174 | 166 | is_project_member=False) |
175 | 167 | |
176 | - if not zclient: | |
168 | + if not zmq: | |
177 | 169 | return |
178 | 170 | |
179 | 171 | user = user or getattr(c, 'user', None) |
@@ -197,7 +189,7 @@ class Globals(object): | ||
197 | 189 | |
198 | 190 | try: |
199 | 191 | if self._zarkov is None: |
200 | - self._zarkov = zclient.ZarkovClient( | |
192 | + self._zarkov = ZarkovClient( | |
201 | 193 | config.get('zarkov.host', 'tcp://127.0.0.1:6543')) |
202 | 194 | self._zarkov.event(event_type, context, extra) |
203 | 195 | except Exception, ex: |
@@ -5,6 +5,7 @@ class NoSuchProjectError(ForgeError): pass | ||
5 | 5 | class NoSuchNeighborhoodError(ForgeError): pass |
6 | 6 | class MailError(ForgeError): pass |
7 | 7 | class AddressException(MailError): pass |
8 | +class NoSuchNBLevelError(ForgeError): pass | |
8 | 9 | |
9 | 10 | class CompoundError(ForgeError): |
10 | 11 | def __repr__(self): |
@@ -1,6 +1,24 @@ | ||
1 | 1 | import calendar |
2 | 2 | from datetime import datetime, timedelta |
3 | 3 | |
4 | +try: | |
5 | + import zmq | |
6 | +except ImportError: | |
7 | + zmq = None | |
8 | +import bson | |
9 | + | |
10 | +class ZarkovClient(object): | |
11 | + | |
12 | + def __init__(self, addr): | |
13 | + context = zmq.Context.instance() | |
14 | + self._sock = context.socket(zmq.PUSH) | |
15 | + self._sock.connect(addr) | |
16 | + | |
17 | + def event(self, type, context, extra=None): | |
18 | + obj = dict( | |
19 | + type=type, context=context, extra=extra) | |
20 | + self._sock.send(bson.BSON.encode(obj)) | |
21 | + | |
4 | 22 | def zero_fill_zarkov_result(zarkov_data, period, start_date, end_date): |
5 | 23 | """Return a new copy of zarkov_data (a dict returned from a zarkov |
6 | 24 | query) with the timeseries data zero-filled for missing dates. |
@@ -36,10 +54,8 @@ def zero_fill_time_series(time_series, period, start_date, end_date): | ||
36 | 54 | time_series (list): A list of [timestamp, value] pairs, e.g.: |
37 | 55 | [[1306886400000.0, 1], [1309478400000.0, 0]] |
38 | 56 | period (str): 'month' or 'date' for monthly or daily timestamps |
39 | - start_date (datetime or str): Start of the date range. If a str is | |
40 | - passed, it must be in %Y-%m-%d format. | |
41 | - end_date (datetime or str): End of the date range. If a str is | |
42 | - passed, it must be in %Y-%m-%d format. | |
57 | + start_date (datetime): Start of the date range. | |
58 | + end_date (datetime or str): End of the date range. | |
43 | 59 | |
44 | 60 | Returns: |
45 | 61 | list. A new copy of time_series, zero-filled. |
@@ -172,8 +172,10 @@ class Thread(Artifact): | ||
172 | 172 | Feed.post(self, title=p.subject, description=p.text) |
173 | 173 | return p |
174 | 174 | |
175 | - def post(self, text, message_id=None, parent_id=None, timestamp=None, **kw): | |
176 | - require_access(self, 'post') | |
175 | + def post(self, text, message_id=None, parent_id=None, | |
176 | + timestamp=None, ignore_security=False, **kw): | |
177 | + if not ignore_security: | |
178 | + require_access(self, 'post') | |
177 | 179 | if self.ref_id and self.artifact: |
178 | 180 | self.artifact.subscribe() |
179 | 181 | if message_id is None: message_id = h.gen_message_id() |
@@ -190,7 +192,7 @@ class Thread(Artifact): | ||
190 | 192 | if timestamp is not None: kwargs['timestamp'] = timestamp |
191 | 193 | if message_id is not None: kwargs['_id'] = message_id |
192 | 194 | post = self.post_class()(**kwargs) |
193 | - if has_access(self, 'unmoderated_post')(): | |
195 | + if ignore_security or has_access(self, 'unmoderated_post')(): | |
194 | 196 | log.info('Auto-approving message from %s', c.user.username) |
195 | 197 | post.approve() |
196 | 198 | else: |
@@ -0,0 +1,10 @@ | ||
1 | +{% if a.has_image_view and b.has_image_view %} | |
2 | + <img src="{{a.url()}}?format=raw" | |
3 | + alt="{{h.text.truncate(a._commit._id, 10)}}" | |
4 | + title="{{h.text.truncate(a._commit._id, 10)}}"/> | |
5 | + <img src="{{b.url()}}?format=raw" | |
6 | + alt="{{h.text.truncate(b._commit._id, 10)}}" | |
7 | + title="{{h.text.truncate(b._commit._id, 10)}}"/> | |
8 | +{% else %} | |
9 | + {{g.highlight(diff, lexer='diff')}} | |
10 | +{% endif %} |
@@ -15,38 +15,47 @@ Commit <a href="{{commit.url()}}">{{commit.shorthand_id()}}</a> {{commit_labels( | ||
15 | 15 | {% endblock %} |
16 | 16 | |
17 | 17 | {% block content %} |
18 | - {{ clone_info(c.app.repo) }} | |
18 | +{{ clone_info(c.app.repo) }} | |
19 | 19 | {{c.revision_widget.display(value=commit, prev=prev, next=next)}} |
20 | 20 | <table> |
21 | 21 | <tbody> |
22 | - {% for diff in commit.diffs.added %} | |
22 | + {% for type, file in artifacts %} | |
23 | 23 | <tr> |
24 | - <td>add</td> | |
25 | - <td><a href="{{commit.url()}}tree{{h.really_unicode(diff)}}">{{h.really_unicode(diff)}}</a></td> | |
26 | - </tr> | |
27 | - {% endfor %}{% for diff in commit.diffs.removed %} | |
28 | - <tr> | |
29 | - <td>remove</td> | |
30 | - <td><a href="{{prev[0].url()}}tree{{h.really_unicode(diff)}}">{{h.really_unicode(diff)}}</a></td> | |
31 | - </tr> | |
32 | - {% endfor %}{% for diff in commit.diffs.changed %} | |
33 | - <tr> | |
34 | - <td>change</td> | |
35 | - <td> | |
36 | - <a href="{{commit.url()}}tree{{h.really_unicode(diff)}}">{{h.really_unicode(diff)}}</a> | |
37 | - <a href="{{commit.url()}}tree{{h.really_unicode(diff)}}?diff={{prev[0].object_id}}">(diff)</a> | |
38 | - </td> | |
39 | - </tr> | |
40 | - {% endfor %}{% for diff in commit.diffs.copied %} | |
41 | - <tr> | |
42 | - <td>copy</td> | |
43 | - <td> | |
44 | - <a href="{{prev[0].url()}}tree{{h.really_unicode(diff.old)}}">{{h.really_unicode(diff.old)}}</a> | |
45 | - <br/>to<br/> | |
46 | - <a href="{{commit.url()}}tree{{h.really_unicode(diff.new)}}">{{h.really_unicode(diff.new)}}</a> | |
47 | - </td> | |
24 | + <td>{{ type }}</td> | |
25 | + <td><a href="#diff-{{loop.index}}">{{h.really_unicode(file)}}</a></td> | |
48 | 26 | </tr> |
49 | 27 | {% endfor %} |
50 | 28 | </tbody> |
51 | 29 | </table> |
30 | + | |
31 | +{% for type, file in artifacts %} | |
32 | + <div class="inline-diff"> | |
33 | + <h6> | |
34 | + {% if type in ('added', 'changed') %} | |
35 | + <a href="{{commit.url()}}tree{{h.really_unicode(file)}}">{{h.really_unicode(file)}}</a> | |
36 | + <a class="commit-diff-link" href="{{commit.url()}}tree{{h.really_unicode(file)}}?diff={{prev[0].object_id}}">Diff</a> | |
37 | + {% elif type == 'removed' %} | |
38 | + <a href="{{prev[0].url()}}tree{{h.really_unicode(file)}}">{{h.really_unicode(file)}}</a> | |
39 | + {% elif type == 'copied' %} | |
40 | + <a href="{{prev[0].url()}}tree{{h.really_unicode(file.old)}}">{{h.really_unicode(file.old)}}</a> | |
41 | + to | |
42 | + <a href="{{commit.url()}}tree{{h.really_unicode(file.new)}}">{{h.really_unicode(file.new)}}</a> | |
43 | + {% endif %} | |
44 | + </h6> | |
45 | + <div id="diff-{{loop.index}}" class="inline-diff-body"> | |
46 | + {% if type != 'removed' %} | |
47 | + <img src="{{g.forge_static('images/spinner.gif')}}" class="loading_icon" alt="Loading..."/> | |
48 | + {% else %} | |
49 | + <span class="empty-diff">File was removed.</span> | |
50 | + {% endif %} | |
51 | + </div> | |
52 | + </div> | |
53 | + {% if type != 'removed' %} | |
54 | + <script type="text/javascript"> | |
55 | + $(document).ready(function() { | |
56 | + $('#diff-{{loop.index}}').load('{{commit.url()}}tree{{h.really_unicode(file)}}?barediff={{prev[0].object_id}}'); | |
57 | + }); | |
58 | + </script> | |
59 | + {% endif %} | |
60 | +{% endfor %} | |
52 | 61 | {% endblock %} |
@@ -2,12 +2,16 @@ | ||
2 | 2 | {% do g.register_forge_css('css/forge/hilite.css') %} |
3 | 3 | |
4 | 4 | {% block title %} |
5 | - {{c.project.name}} / {{c.app.config.options.mount_label}} / Diff of {{h.really_unicode(a.path())}} | |
5 | + {{c.project.name}} / {{c.app.config.options.mount_label}} / Diff of {{h.really_unicode(b.path())}} | |
6 | 6 | {% endblock %} |
7 | 7 | |
8 | 8 | {% block header %}Diff of |
9 | 9 | <a href="{{b.url()}}">{{b.path()}}</a> |
10 | +{% if a %} | |
10 | 11 | <a href="{{a.url()}}">{{a.commit.shorthand_id()}}</a> |
12 | +{% else %} | |
13 | +[000000] | |
14 | +{% endif %} | |
11 | 15 | .. |
12 | 16 | <a href="{{b.url()}}">{{b.commit.shorthand_id()}}</a> |
13 | 17 | {% endblock %} |
@@ -1,38 +1,45 @@ | ||
1 | 1 | {% from 'allura:templates/jinja_master/lib.html' import email_gravatar, abbr_date with context %} |
2 | -<p> | |
3 | -{% if value.author_url %} | |
4 | - Authored by | |
5 | - <a href="{{value.author_url}}">{{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}}</a> | |
6 | - <a href="{{value.author_url}}">{{h.really_unicode(value.authored.name)}}</a> | |
7 | -{% else %} | |
8 | - Authored by | |
9 | - {{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}} {{h.really_unicode(value.authored.name)}} | |
10 | -{% endif %} | |
11 | -{% if value.authored.date %}{{abbr_date(value.authored.date)}}{% endif %} | |
12 | -{% if value.committed.email != value.authored.email %} | |
13 | - {% if value.committer_url %} | |
14 | - Committed by | |
15 | - <a href="{{value.committer_url}}">{{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}}</a> | |
16 | - <a href="{{value.committer_url}}">{{h.really_unicode(value.committed.name)}}</a> | |
17 | - {% else %} | |
18 | - Committed by | |
19 | - {{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}} {{h.really_unicode(value.committed.name)}} | |
20 | - {% endif %} | |
21 | - {% if value.committed.date %}{{abbr_date(value.committed.date)}}{% endif %} | |
22 | -{% endif %} | |
23 | -</p> | |
2 | +<div class="commit-details"> | |
3 | + <div class="commit-message"> | |
4 | + <div class="first-line">{{g.markdown.convert(h.really_unicode(value.message.split('\n')[0]))}}</div> | |
5 | + {{g.markdown.convert(h.really_unicode('\n'.join(value.message.split('\n')[1:])))}} | |
6 | + </div> | |
7 | + <h2 class="commit-details"> | |
8 | + <ul class="commit-links"> | |
9 | + <li><a class="commit-tree-link" href="{{value.url()}}tree/">Tree</a></li> | |
10 | + {% if prev %} | |
11 | + <li class="commit-parents"> | |
12 | + Parent(s): | |
13 | + {% for ci in prev %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %} | |
14 | + </li> | |
15 | + {% endif %} | |
16 | + {% if next %} | |
17 | + <li class="commit-children"> | |
18 | + Child(ren): | |
19 | + {% for ci in next %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %} | |
20 | + </li> | |
21 | + {% endif %} | |
22 | + </ul> | |
24 | 23 | |
25 | -<p><a href="{{value.url()}}tree/">Tree</a></p> | |
26 | -{% if prev %} | |
27 | -<p> | |
28 | - Parent(s): | |
29 | - {% for ci in prev %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %} | |
30 | -</p> | |
31 | -{% endif %} | |
32 | -{% if next %} | |
33 | -<p> | |
34 | - Child(ren): | |
35 | - {% for ci in next %}<a href="{{ci.url()}}">{{ci.shorthand_id()}}</a>{% endfor %} | |
36 | -</p> | |
37 | -{% endif %} | |
38 | -{{g.markdown.convert(h.really_unicode(value.message))}} | |
24 | + Authored by | |
25 | + {% if value.author_url %} | |
26 | + <a href="{{value.author_url}}">{{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}}</a> | |
27 | + <a href="{{value.author_url}}">{{h.really_unicode(value.authored.name)}}</a> | |
28 | + {% else %} | |
29 | + {{email_gravatar(value.authored.email, title=h.really_unicode(value.authored.name), size=16)}} {{h.really_unicode(value.authored.name)}} | |
30 | + {% endif %} | |
31 | + | |
32 | + {% if value.authored.date %}{{abbr_date(value.authored.date)}}{% endif %} | |
33 | + | |
34 | + {% if value.committed.email != value.authored.email %} | |
35 | + Committed by | |
36 | + {% if value.committer_url %} | |
37 | + <a href="{{value.committer_url}}">{{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}}</a> | |
38 | + <a href="{{value.committer_url}}">{{h.really_unicode(value.committed.name)}}</a> | |
39 | + {% else %} | |
40 | + {{email_gravatar(value.committed.email, title=h.really_unicode(value.committed.name), size=16)}} {{h.really_unicode(value.committed.name)}} | |
41 | + {% endif %} | |
42 | + {% if value.committed.date %}{{abbr_date(value.committed.date)}}{% endif %} | |
43 | + {% endif %} | |
44 | + </h2> | |
45 | +</div> |
@@ -11,7 +11,7 @@ from nose.tools import assert_raises, assert_equals, with_setup | ||
11 | 11 | import mock |
12 | 12 | |
13 | 13 | from ming.orm.ormsession import ThreadLocalORMSession |
14 | -from webob import Request, Response | |
14 | +from webob import Request, Response, exc | |
15 | 15 | |
16 | 16 | from allura import model as M |
17 | 17 | from allura.lib.app_globals import Globals |
@@ -192,4 +192,14 @@ def test_post_delete(): | ||
192 | 192 | ThreadLocalORMSession.flush_all() |
193 | 193 | p.delete() |
194 | 194 | |
195 | - | |
195 | +@with_setup(setUp, tearDown) | |
196 | +def test_post_permission_check(): | |
197 | + d = M.Discussion(shortname='test', name='test') | |
198 | + t = M.Thread(discussion_id=d._id, subject='Test Thread') | |
199 | + c.user = M.User.anonymous() | |
200 | + try: | |
201 | + p1 = t.post('This post will fail the check.') | |
202 | + assert False, "Expected an anonymous post to fail." | |
203 | + except exc.HTTPUnauthorized: | |
204 | + pass | |
205 | + p2 = t.post('This post will pass the check.', ignore_security=True) | |
\ No newline at end of file |
@@ -4,7 +4,7 @@ from datadiff.tools import assert_equal | ||
4 | 4 | import pylons |
5 | 5 | |
6 | 6 | from alluratest.controller import setup_basic_test, setup_global_objects |
7 | -from allura.command import script | |
7 | +from allura.command import script, set_neighborhood_level | |
8 | 8 | from allura import model as M |
9 | 9 | |
10 | 10 |
@@ -23,3 +23,14 @@ def test_script(): | ||
23 | 23 | cmd.run([test_config, 'allura/tests/tscript.py' ]) |
24 | 24 | cmd.command() |
25 | 25 | assert_raises(ValueError, cmd.run, [test_config, 'allura/tests/tscript_error.py' ]) |
26 | + | |
27 | +def test_set_neighborhood_level(): | |
28 | + neighborhood = M.Neighborhood.query.find().first() | |
29 | + n_id = neighborhood._id | |
30 | + | |
31 | + cmd = set_neighborhood_level.SetNeighborhoodLevelCommand('setnblevel') | |
32 | + cmd.run([test_config, str(n_id), 'gold']) | |
33 | + cmd.command() | |
34 | + | |
35 | + neighborhood = M.Neighborhood.query.get(_id=n_id) | |
36 | + assert neighborhood.level == 'gold' |
@@ -0,0 +1,110 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +import unittest | |
3 | +from calendar import timegm | |
4 | +from datetime import datetime | |
5 | + | |
6 | +import bson | |
7 | +import mock | |
8 | + | |
9 | +from allura.lib import zarkov_helpers as zh | |
10 | + | |
11 | +class TestZarkovClient(unittest.TestCase): | |
12 | + | |
13 | + def setUp(self): | |
14 | + addr = 'tcp://0.0.0.0:0' | |
15 | + ctx = mock.Mock() | |
16 | + self.socket = mock.Mock() | |
17 | + ctx.socket = mock.Mock(return_value=self.socket) | |
18 | + PUSH=mock.Mock() | |
19 | + with mock.patch('allura.lib.zarkov_helpers.zmq') as zmq: | |
20 | + zmq.PUSH=PUSH | |
21 | + zmq.Context.instance.return_value = ctx | |
22 | + self.client = zh.ZarkovClient(addr) | |
23 | + zmq.Context.instance.assert_called_once_with() | |
24 | + ctx.socket.assert_called_once_with(PUSH) | |
25 | + self.socket.connect.assert_called_once_with(addr) | |
26 | + | |
27 | + def test_event(self): | |
28 | + self.client.event('test', dict(user='testuser')) | |
29 | + obj = bson.BSON.encode(dict( | |
30 | + type='test', | |
31 | + context=dict(user='testuser'), | |
32 | + extra=None)) | |
33 | + self.socket.send.assert_called_once_with(obj) | |
34 | + | |
35 | +class TestZeroFill(unittest.TestCase): | |
36 | + | |
37 | + def setUp(self): | |
38 | + self.dt_begin = datetime(2010, 6, 1) | |
39 | + self.dt_end = datetime(2011, 7, 1) | |
40 | + ts_begin = timegm(self.dt_begin.timetuple()) | |
41 | + ts_end = timegm(self.dt_end.timetuple()) | |
42 | + self.ts_ms_begin = ts_begin * 1000.0 | |
43 | + self.ts_ms_end = ts_end * 1000.0 | |
44 | + self.zarkov_data = dict( | |
45 | + a=dict( | |
46 | + a1=[ (self.ts_ms_begin, 1000), (self.ts_ms_end, 1000) ], | |
47 | + a2=[ (self.ts_ms_begin, 1000), (self.ts_ms_end, 1000) ] ), | |
48 | + b=dict( | |
49 | + b1=[ (self.ts_ms_begin, 2000), (self.ts_ms_end, 2000) ], | |
50 | + b2=[ (self.ts_ms_begin, 2000), (self.ts_ms_end, 2000) ] )) | |
51 | + | |
52 | + def test_to_utc_timestamp(self): | |
53 | + self.assertEqual( | |
54 | + zh.to_utc_timestamp(self.dt_begin), | |
55 | + self.ts_ms_begin) | |
56 | + self.assertEqual( | |
57 | + zh.to_utc_timestamp(self.dt_end), | |
58 | + self.ts_ms_end) | |
59 | + | |
60 | + def test_zero_fill_time_series_month(self): | |
61 | + result = zh.zero_fill_time_series( | |
62 | + self.zarkov_data['a']['a1'], 'month', | |
63 | + datetime(2010, 5, 1), datetime(2011, 9, 1)) | |
64 | + self.assertEqual(result[0][1], 0) | |
65 | + self.assertEqual(result[-1][1], 0) | |
66 | + self.assertEqual(len(result), 17) | |
67 | + self.assertEqual(result[1][1], 1000) | |
68 | + self.assertEqual(result[-3][1], 1000) | |
69 | + days_ms = 24 * 3600 * 1000 | |
70 | + min_delta = 28 * days_ms | |
71 | + max_delta= 31 * days_ms | |
72 | + for p1, p2 in zip(result, result[1:]): | |
73 | + delta = p2[0]-p1[0] | |
74 | + assert min_delta <= delta <= max_delta, delta | |
75 | + | |
76 | + def test_zero_fill_time_series_date(self): | |
77 | + result = zh.zero_fill_time_series( | |
78 | + self.zarkov_data['a']['a1'], 'date', | |
79 | + datetime(2010, 5, 1), datetime(2011, 9, 1)) | |
80 | + self.assertEqual(len(result), 489) | |
81 | + days_ms = 24 * 3600 * 1000 | |
82 | + for p1, p2 in zip(result, result[1:]): | |
83 | + delta = p2[0]-p1[0] | |
84 | + assert delta == days_ms | |
85 | + | |
86 | + def test_zero_fill_zarkov_month_dt(self): | |
87 | + result = zh.zero_fill_zarkov_result( | |
88 | + self.zarkov_data, 'month', | |
89 | + datetime(2010, 5, 1), datetime(2011, 9, 1)) | |
90 | + a_result = result['a']['a1'] | |
91 | + b_result = result['b']['b2'] | |
92 | + self.assertEqual(a_result[0][1], 0) | |
93 | + self.assertEqual(a_result[-1][1], 0) | |
94 | + self.assertEqual(len(a_result), 17) | |
95 | + self.assertEqual(a_result[1][1], 1000) | |
96 | + self.assertEqual(a_result[-3][1], 1000) | |
97 | + self.assertEqual(b_result[0][1], 0) | |
98 | + self.assertEqual(b_result[-1][1], 0) | |
99 | + self.assertEqual(len(b_result), 17) | |
100 | + self.assertEqual(b_result[1][1], 2000) | |
101 | + self.assertEqual(b_result[-3][1], 2000) | |
102 | + | |
103 | + def test_zero_fill_zarkov_month_str(self): | |
104 | + result0 = zh.zero_fill_zarkov_result( | |
105 | + self.zarkov_data, 'month', | |
106 | + datetime(2010, 5, 1), datetime(2011, 9, 1)) | |
107 | + result1 = zh.zero_fill_zarkov_result( | |
108 | + self.zarkov_data, 'month', | |
109 | + '2010-5-1', '2011-09-1') | |
110 | + self.assertEqual(result0, result1) |
@@ -16,7 +16,7 @@ | ||
16 | 16 | {{c.subscribe_form.display(value=subscribed, action='subscribe', style='icon')}} |
17 | 17 | {% endif %} |
18 | 18 | {% if allow_edit %} |
19 | - <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
19 | + <a href="{{tg.url(c.app.url+'edit/', dict(q=url_q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
20 | 20 | {% endif %} |
21 | 21 | {% endblock %} |
22 | 22 |
@@ -13,7 +13,7 @@ | ||
13 | 13 | {% block header %}ForgeTracker for {{c.project.shortname}}{% endblock %} |
14 | 14 | |
15 | 15 | {% block actions %} |
16 | - <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}} active"></b></a> | |
16 | + <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}} active"></b></a> | |
17 | 17 | {% endblock %} |
18 | 18 | |
19 | 19 | {% block edit_box %} |
@@ -8,7 +8,7 @@ | ||
8 | 8 | |
9 | 9 | {% block actions %} |
10 | 10 | {% if allow_edit %} |
11 | - <a href="{{tg.url('edit/', dict(q=q, limit=limit, sort=sort))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
11 | + <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
12 | 12 | {% endif %} |
13 | 13 | {% endblock %} |
14 | 14 |
@@ -13,7 +13,7 @@ | ||
13 | 13 | {% block actions %} |
14 | 14 | <a href="{{tg.url(c.app.url+'search_feed/', dict(q=q, limit=limit, sort=sort))}}" title="Feed"><b data-icon="{{g.icons['feed'].char}}" class="ico {{g.icons['feed'].css}}"></b></a> |
15 | 15 | {% if allow_edit and count != 0 %} |
16 | - <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
16 | + <a href="{{tg.url(c.app.url+'edit/', dict(q=q, limit=limit, sort=sort, page=page))}}" title="Bulk Edit"><b data-icon="{{g.icons['pencil'].char}}" class="ico {{g.icons['pencil'].css}}"></b></a> | |
17 | 17 | {% endif %} |
18 | 18 | {% endblock %} |
19 | 19 |
@@ -1,5 +1,6 @@ | ||
1 | 1 | <div> |
2 | - {% if tickets.__len__() %} | |
2 | + {% if tickets|length %} | |
3 | + {{widget.fields['page_size'].display(page=page, count=count, limit=limit)}} | |
3 | 4 | <table> |
4 | 5 | <thead> |
5 | 6 | <tr> |
@@ -26,6 +27,8 @@ | ||
26 | 27 | {% endfor %} |
27 | 28 | </tbody> |
28 | 29 | </table> |
30 | + {{widget.fields['lightbox'].display()}} | |
31 | + {{widget.fields['page_list'].display(limit=limit, page=page, count=count)}} | |
29 | 32 | <script type="text/javascript"> |
30 | 33 | var q="{{query}}", count={{count}}, limit={{limit}}, page={{page}}, sort="{{sort}}"; |
31 | 34 | </script> |
@@ -1,7 +1,7 @@ | ||
1 | 1 | <form id="update-values"> |
2 | 2 | {% for field in globals.custom_fields %} |
3 | 3 | {% if field.name == '_milestone' %} |
4 | - <div class="grid-4">{{milestones}} | |
4 | + <div class="grid-6">{{milestones}} | |
5 | 5 | <label for="{{field.name}}" class="cr">{{field.label}}:</label> |
6 | 6 | <select name="{{field.name}}" class="wide"> |
7 | 7 | <option value="" selected="selected">no change</option> |
@@ -14,7 +14,7 @@ | ||
14 | 14 | </div> |
15 | 15 | {% endif %} |
16 | 16 | {% endfor %} |
17 | - <div class="grid-4"> | |
17 | + <div class="grid-6"> | |
18 | 18 | <label for="status" class="cr">Status:</label> |
19 | 19 | <select name="status" class="wide"> |
20 | 20 | <option value="" selected="selected">no change</option> |
@@ -23,17 +23,17 @@ | ||
23 | 23 | {% endfor %} |
24 | 24 | </select> |
25 | 25 | </div> |
26 | - <div class="grid-4"> | |
26 | + <div class="grid-6"> | |
27 | 27 | <label for="assigned_to" class="cr">Owner:</label> |
28 | 28 | {{c.user_select.display(name='assigned_to', value='', className='wide')}} |
29 | 29 | </div> |
30 | - {% set cf_count = -1 %} | |
30 | + {% set cf_count = 0 %} | |
31 | 31 | {% for field in globals.custom_fields %} |
32 | 32 | {% if field.type != 'sum' and field.name != '_milestone' %} |
33 | - {% if cf_count%4 == 0 %} | |
33 | + {% if cf_count%3 == 0 %} | |
34 | 34 | <div style="clear: both"></div> |
35 | 35 | {% endif %} |
36 | - <div class="grid-4"> | |
36 | + <div class="grid-6"> | |
37 | 37 | <label for="{{field.id}}" class="cr">{{field.label}}:</label> |
38 | 38 | {% if field.type == 'boolean' %} |
39 | 39 | <input name="{{field.name}}" type="checkbox" value="True"/> |
@@ -64,7 +64,7 @@ | ||
64 | 64 | {% set cf_count=cf_count+1 %} |
65 | 65 | {% endif %} |
66 | 66 | {% endfor %} |
67 | - <div class="grid-16"> | |
67 | + <div class="grid-18"> | |
68 | 68 | <input type="button" onclick="update_tickets()" value="Save"/> |
69 | 69 | <a href="{{cancel_href}}" class="btn link">Cancel</a> |
70 | 70 | <!-- tg.url(c.app.url+'search/', dict(q=query, limit=limit, sort=sort))}}" class="btn link">Cancel</a>--> |
@@ -382,7 +382,7 @@ class RootController(BaseController): | ||
382 | 382 | @with_trailing_slash |
383 | 383 | @h.vardec |
384 | 384 | @expose('jinja:forgetracker:templates/tracker/index.html') |
385 | - def index(self, limit=25, columns=None, page=0, sort='ticket_num desc', **kw): | |
385 | + def index(self, limit=25, columns=None, page=0, sort='ticket_num_i asc', **kw): | |
386 | 386 | kw.pop('q', None) # it's just our original query mangled and sent back to us |
387 | 387 | result = TM.Ticket.paged_query(c.app.globals.not_closed_mongo_query, |
388 | 388 | sort=sort, limit=int(limit), |
@@ -391,6 +391,7 @@ class RootController(BaseController): | ||
391 | 391 | result['subscribed'] = M.Mailbox.subscribed() |
392 | 392 | result['allow_edit'] = has_access(c.app, 'write')() |
393 | 393 | result['help_msg'] = c.app.config.options.get('TicketHelpSearch') |
394 | + result['url_q'] = c.app.globals.not_closed_query | |
394 | 395 | c.ticket_search_results = W.ticket_search_results |
395 | 396 | return result |
396 | 397 |
@@ -584,7 +585,7 @@ class RootController(BaseController): | ||
584 | 585 | sort=validators.UnicodeString(if_empty='ticket_num_i asc'))) |
585 | 586 | def edit(self, q=None, limit=None, page=None, sort=None, **kw): |
586 | 587 | require_access(c.app, 'write') |
587 | - result = self.paged_query(q, sort=sort, **kw) | |
588 | + result = self.paged_query(q, sort=sort, limit=limit, page=page, **kw) | |
588 | 589 | # if c.app.globals.milestone_names is None: |
589 | 590 | # c.app.globals.milestone_names = '' |
590 | 591 | result['globals'] = c.app.globals |
@@ -32,10 +32,10 @@ class TicketSearchResults(ew_core.SimpleForm): | ||
32 | 32 | for r in super(TicketSearchResults, self).resources(): |
33 | 33 | yield r |
34 | 34 | |
35 | -class MassEdit(ew_core.Widget): | |
35 | +class MassEdit(ew_core.SimpleForm): | |
36 | 36 | template='jinja:forgetracker:templates/tracker_widgets/mass_edit.html' |
37 | 37 | defaults=dict( |
38 | - ew_core.Widget.defaults, | |
38 | + ew_core.SimpleForm.defaults, | |
39 | 39 | count=None, |
40 | 40 | limit=None, |
41 | 41 | query=None, |
@@ -43,8 +43,16 @@ class MassEdit(ew_core.Widget): | ||
43 | 43 | page=1, |
44 | 44 | sort=None) |
45 | 45 | |
46 | + class fields(ew_core.NameList): | |
47 | + page_list=ffw.PageList() | |
48 | + page_size=ffw.PageSize() | |
49 | + lightbox=ffw.Lightbox(name='col_list',trigger='#col_menu') | |
50 | + | |
46 | 51 | def resources(self): |
47 | 52 | yield ew.JSLink('tracker_js/ticket-list.js') |
53 | + yield ew.CSSLink('tracker_css/ticket-list.css') | |
54 | + for r in super(MassEdit, self).resources(): | |
55 | + yield r | |
48 | 56 | |
49 | 57 | class MassEditForm(ew_core.Widget): |
50 | 58 | template='jinja:forgetracker:templates/tracker_widgets/mass_edit_form.html' |
@@ -9,10 +9,6 @@ phpserialize==1.2 | ||
9 | 9 | psycopg2==2.2.2 |
10 | 10 | sf.phpsession==0.1 |
11 | 11 | pyzmq==2.1.7 |
12 | -Zarkov==0.1.0dev-20110815 | |
13 | -gevent==0.13.6 | |
14 | -greenlet==0.3.1 | |
15 | -gevent-zeromq==0.2.0 | |
16 | 12 | Pygments==1.4sf1-20110601 |
17 | 13 | |
18 | 14 | # for the migration scripts only |