diff --git a/pandora/app/models.py b/pandora/app/models.py index 1b1344356..a60ab14c2 100644 --- a/pandora/app/models.py +++ b/pandora/app/models.py @@ -3,7 +3,6 @@ from __future__ import division, with_statement from django.db import models -from django.contrib.auth.models import User class Page(models.Model): @@ -15,22 +14,3 @@ class Page(models.Model): def __unicode__(self): return self.name -class Log(models.Model): - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - user = models.ForeignKey(User, default=None, blank=True, null=True) - url = models.CharField(max_length=1000, default='') - line = models.IntegerField(default=0) - text = models.TextField(blank=True) - - def __unicode__(self): - return u"%s" % self.id - - def json(self): - return { - 'created': self.created, - 'modified': self.modified, - 'user': self.user and self.user.username or '', - 'type': self.type, - 'message': self.message, - } diff --git a/pandora/app/views.py b/pandora/app/views.py index f07e49ff5..71decff8e 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -92,35 +92,3 @@ def redirect_url(request, url): else: return HttpResponse(''%json.dumps(url)) -def log(request): - ''' - param data { - url: url - line: line - text: text - } - return { - status: ... - data: { - name: - body: - } - } - ''' - data = json.loads(request.POST['data']) - if request.user.is_authenticated(): - user = request.user - else: - user = None - if 'text' in data: - l = models.Log( - text=data['text'], - line=int(data.get('line', 0)), - url=data.get('url', '') - ) - if user: - l.user = user - l.save() - response = json_response() - return render_to_json_response(response) -actions.register(log) diff --git a/pandora/log/__init__.py b/pandora/log/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pandora/log/managers.py b/pandora/log/managers.py new file mode 100644 index 000000000..19ed1d557 --- /dev/null +++ b/pandora/log/managers.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from django.db.models import Q, Manager +from ox.django.query import QuerySet +import ox + +def parseCondition(condition, user): + ''' + condition: { + value: "war" + } + or + condition: { + key: "year", + value: "1970-1980, + operator: "!=" + } + ... + ''' + k = condition.get('key', 'name') + k = { + 'user': 'user__username', + }.get(k, k) + if not k: + k = 'name' + v = condition['value'] + op = condition.get('operator') + if not op: + op = '=' + if op.startswith('!'): + op = op[1:] + exclude = True + else: + exclude = False + if op == '-': + q = parseCondition({'key': k, 'value': v[0], 'operator': '>='}, user) \ + & parseCondition({'key': k, 'value': v[1], 'operator': '<'}, user) + if exclude: + return ~q + else: + return q + if k == 'id': + v = ox.from26(v) + if isinstance(v, bool): #featured and public flag + key = k + elif k in ('id', ): + key = '%s%s' % (k, { + '>': '__gt', + '>=': '__gte', + '<': '__lt', + '<=': '__lte', + }.get(op,'')) + else: + key = '%s%s' % (k, { + '==': '__iexact', + '^': '__istartswith', + '$': '__iendswith', + }.get(op,'__icontains')) + + key = str(key) + if exclude: + q = ~Q(**{key: v}) + else: + q = Q(**{key: v}) + return q + +def parseConditions(conditions, operator, user): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + + +class LogManager(Manager): + + def get_query_set(self): + return QuerySet(self.model) + + def find(self, data, user): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + + query = data.get('query', {}) + conditions = parseConditions(query.get('conditions', []), + query.get('operator', '&'), + user) + if conditions: + qs = qs.filter(conditions) + return qs diff --git a/pandora/log/models.py b/pandora/log/models.py new file mode 100644 index 000000000..14eb15280 --- /dev/null +++ b/pandora/log/models.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division, with_statement + +from django.db import models +from django.contrib.auth.models import User +import ox + +import managers + +class Log(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + user = models.ForeignKey(User, default=None, blank=True, null=True) + url = models.CharField(max_length=1000, default='') + line = models.IntegerField(default=0) + text = models.TextField(blank=True) + + objects = managers.LogManager() + + def __unicode__(self): + return u"%s" % self.id + + def json(self, keys=None): + j = { + 'created': self.created, + 'id': ox.to26(self.id), + 'line': self.line, + 'modified': self.modified, + 'text': self.text, + 'url': self.url, + 'user': self.user and self.user.username or '', + } + if keys: + for key in j.keys(): + if key not in keys: + del j[key] + return j + diff --git a/pandora/app/log.py b/pandora/log/utils.py similarity index 100% rename from pandora/app/log.py rename to pandora/log/utils.py diff --git a/pandora/log/views.py b/pandora/log/views.py new file mode 100644 index 000000000..89825a848 --- /dev/null +++ b/pandora/log/views.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division + +import ox +from ox.utils import json + +from ox.django.decorators import login_required_json, admin_required_json +from ox.django.shortcuts import render_to_json_response, get_object_or_404_json, json_response + +from api.actions import actions + +from item import utils + +import models + + +def log(request): + ''' + param data { + url: url + line: line + text: text + } + return { + status: ... + data: { + name: + body: + } + } + ''' + data = json.loads(request.POST['data']) + if request.user.is_authenticated(): + user = request.user + else: + user = None + url = data.get('url', '').split('/static/')[-1] + if 'text' in data: + l = models.Log( + text=data['text'], + line=int(data.get('line', 0)), + url=url + ) + if user: + l.user = user + l.save() + response = json_response() + return render_to_json_response(response) +actions.register(log) + + +@admin_required_json +def removeLogs(request): + ''' + param data { + 'ids': , + } + can contain any of the allowed keys for place + ''' + data = json.loads(request.POST['data']) + models.Log.objects.filter(id__in=[ox.from26(i) for i in data['ids']]).delete() + response = json_response() + return render_to_json_response(response) +actions.register(removeLogs, cache=False) + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key':'name', 'operator':'+'}] + for key in ('keys', 'group', 'list', 'range', 'sort', 'query'): + if key in data: + query[key] = data[key] + query['qs'] = models.Log.objects.find(query, user) + return query + +def order_query(qs, sort): + order_by = [] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + }.get(e['key'], e['key']) + order = '%s%s' % (operator, key) + order_by.append(order) + if order_by: + qs = qs.order_by(*order_by, nulls_last=True) + return qs + +@admin_required_json +def findLogs(request): + ''' + param data { + query: { + conditions: [ + { + key: 'user', + value: 'something', + operator: '=' + } + ] + operator: "," + }, + sort: [{key: 'created', operator: '+'}], + range: [0, 100] + keys: [] + } + + ''' + data = json.loads(request.POST['data']) + response = json_response() + + query = parse_query(data, request.user) + qs = order_query(query['qs'], query['sort']) + qs = qs.distinct() + if 'keys' in data: + qs = qs.select_related() + qs = qs[query['range'][0]:query['range'][1]] + response['data']['items'] = [p.json(data['keys']) for p in qs] + elif 'position' in query: + ids = [i.get_id() for i in qs] + data['conditions'] = data['conditions'] + { + 'value': data['position'], + 'key': query['sort'][0]['key'], + 'operator': '^' + } + query = parse_query(data, request.user) + qs = order_query(query['qs'], query['sort']) + if qs.count() > 0: + response['data']['position'] = utils.get_positions(ids, [qs[0].itemId])[0] + elif 'positions' in data: + ids = [ox.to26(i.id) for i in qs] + response['data']['positions'] = utils.get_positions(ids, data['positions']) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findLogs, cache=False) diff --git a/pandora/settings.py b/pandora/settings.py index e4cda5576..1848c0b76 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -126,6 +126,7 @@ INSTALLED_APPS = ( # 'south', 'djcelery', 'app', + 'log', 'annotation', 'clip', @@ -151,7 +152,7 @@ LOGGING = { 'handlers': { 'errors': { 'level': 'ERROR', - 'class': 'pandora.app.log.ErrorHandler' + 'class': 'pandora.log.utils.ErrorHandler' } }, 'loggers': { diff --git a/static/js/pandora/logsDialog.js b/static/js/pandora/logsDialog.js new file mode 100644 index 000000000..41be3a8be --- /dev/null +++ b/static/js/pandora/logsDialog.js @@ -0,0 +1,247 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript + +pandora.ui.logsDialog = function() { + + var height = Math.round((window.innerHeight - 48) * 0.9), + width = Math.round(window.innerWidth * 0.9), + numberOfLogs = 0, + + $findSelect = Ox.Select({ + items: [ + {id: 'all', title: 'Find: All', checked: true}, + {id: 'url', title: 'Find: Url'}, + {id: 'user', title: 'Find: User'} + ], + overlap: 'right', + type: 'image' + }) + .bindEvent({ + change: function(data) { + var key = data.selected[0].id, + value = $findInput.value(); + value && updateList(key, value); + $findInput.options({ + placeholder: data.selected[0].title + }); + } + }), + + $findInput = Ox.Input({ + changeOnKeypress: true, + clear: true, + placeholder: 'Find: All', + width: 192 + }) + .bindEvent({ + change: function(data) { + var key = $findSelect.value(), + value = data.value; + updateList(key, value); + } + }), + + $findElement = Ox.FormElementGroup({ + elements: [ + $findSelect, + $findInput + ] + }) + .css({float: 'right', margin: '4px'}), + + $list = Ox.TextList({ + columns: [ + { + id: 'id', + title: 'ID', + unique: true, + visible: false, + }, + { + id: 'url', + title: 'URL', + operator: '+', + visible: true, + width: 420 + }, + { + id: 'line', + title: 'Line', + operator: '+', + visible: true, + width: 48 + }, + { + format: function(value) { + return value.replace(/[TZ]/g, ' '); + }, + align: 'right', + id: 'created', + operator: '-', + title: 'Date', + visible: true, + width: 128 + }, + { + id: 'user', + title: 'User', + visible: false, + width: 128 + }, + { + id: 'text', + title: 'Text', + visible: false, + width: 300 + }, + ], + columnsRemovable: true, + columnsVisible: true, + items: pandora.api.findLogs, + keys: ['text'], + scrollbarVisible: true, + sort: [ + {key: 'created', operator: '-'} + ] + }) + .bindEvent({ + init: function(data) { + numberOfLogs = data.items; + $status.html( + Ox.formatNumber(numberOfLogs) + + ' log ' + (numberOfLogs == 1 ? 'entry' : 'entries') + ); + }, + select: function(data) { + var values; + $log.empty(); + if (data.ids.length) { + values = $list.value(data.ids[0]); + $logLabel.options({ + title: values.url + }); + $log.append(renderLog(values)); + } else { + $logLabel.options({title: 'No logs selected'}); + } + }, + 'delete': function(data) { + pandora.api.removeLogs({ids: data.ids}, function(result) { + $list.reloadList(); + }); + } + }), + + $logLabel = Ox.Label({ + textAlign: 'center', + title: 'No logs selected', + width: 604 + }) + .css({margin: '4px'}), + + $log = Ox.Element({}), + + that = Ox.Dialog({ + buttons: [ + Ox.Button({ + id: 'done', + title: 'Done', + width: 48 + }).bindEvent({ + click: function() { + that.close(); + } + }) + ], + closeButton: true, + content: Ox.SplitPanel({ + elements: [ + { + element: Ox.SplitPanel({ + elements: [ + { + element: Ox.Bar({size: 24}) + .append($status) + .append( + $findElement + ), + size: 24 + }, + { + element: $list + } + ], + orientation: 'vertical' + }) + }, + { + element: Ox.SplitPanel({ + elements: [ + { + element: Ox.Bar({size: 24}) + .append($logLabel), + size: 24 + }, + { + element: $log + } + ], + orientation: 'vertical' + }), + size: 612 + } + ], + orientation: 'horizontal' + }), + height: height, + maximizeButton: true, + minHeight: 256, + minWidth: 512, + padding: 0, + title: 'Manage Logs', + width: width + }), + + $status = $('
') + .css({ + position: 'absolute', + top: '4px', + left: '4px', + right: '256px', + bottom: '4px', + paddingTop: '2px', + fontSize: '9px', + textAlign: 'center', + }) + .appendTo(that.$element.find('.OxButtonsbar')); + + + function renderLog(logData) { + var $checkbox; + return Ox.Element() + .css({ + padding: '8px' + }) + .append($('
').html(logData.text));
+    }
+
+    function updateList(key, value) {
+        var query = {
+                conditions: Ox.merge(
+                    key != 'url' ? [{key: 'user', value: value, operator: '='}] : [],
+                    key != 'user' ? [{key: 'url', value: value, operator: '='}] : []
+                ),
+                operator: key == 'all' ? '|' : '&'
+            };
+        $list.options({
+            items: function(data, callback) {
+                return pandora.api.findLogs(Ox.extend(data, {
+                    query: query
+                }), callback);
+            }
+        });
+    }
+
+    return that;
+
+};
+
diff --git a/static/js/pandora/menu.js b/static/js/pandora/menu.js
index 77905afdc..3c685e912 100644
--- a/static/js/pandora/menu.js
+++ b/static/js/pandora/menu.js
@@ -144,6 +144,7 @@ pandora.ui.mainMenu = function() {
                             { id: 'clearcache', title: 'Clear Cache'},
                             { id: 'reloadapplication', title: 'Reload Application'},
                             { id: 'resetui', title: 'Reset UI Settings'},
+                            { id: 'logs', title: 'View Logs...'},
                             { id: 'debug', title: (localStorage.debug?'Disable':'Enable')+' Debug Mode'}
                         ] }
                     ]
@@ -252,6 +253,8 @@ pandora.ui.mainMenu = function() {
                         groups: pandora.site.user.ui.groups
                     });
                     pandora.$ui.contentPanel.replaceElement(0, pandora.$ui.browser = pandora.ui.browser());
+                } else if (data.id == 'logs') {
+                    pandora.$ui.logsDialog = pandora.ui.logsDialog().open();
                 } else if (data.id == 'clearcache') {
                     Ox.Request.clearCache();
                 } else if (data.id == 'reloadapplication') {