Fixed password change not allowed for non Privilaged users.

Fixed storing utc dates in message tables.
Created utc class for time-aware dates.
Updated home to show age of Threads and open in new window available.
Modified message detail template to show editable by privileged user.
Added subscribers to thread detail.
Improved markup for message-detail to use less space.
Improved the chosen directive for when there is no create option.
Added method 'Names' to user resource for use in message subscribers.
Payment and Receipt clear typeahead on adding ledgers.


TODO: Implement thread read status per user.
TODO: Implemented edit post functionality for privileged user.
TODO: Implement thread filtering on main page for subscribers

Signed-off-by: Tanshu <tanshu@gmail.com>
This commit is contained in:
Tanshu 2013-06-06 16:56:05 +05:30
parent d5dcd392f7
commit 01298ebadb
14 changed files with 268 additions and 128 deletions

View File

@ -130,6 +130,10 @@ class User(Base):
def list(cls):
return DBSession.query(cls).order_by(cls.name).all()
@classmethod
def query(cls):
return DBSession.query(cls)
@classmethod
def filtered_list(cls, name):
query = DBSession.query(cls)

View File

@ -63,7 +63,7 @@ class Thread(Base):
def __init__(self, title=None, creation_date=None, user_id=None, priority=5, public=False, closed=False):
self.title = title
self.creation_date = datetime.now() if creation_date is None else creation_date
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
self.user_id = user_id
self.priority = priority
self.public = public
@ -96,7 +96,7 @@ class Post(Base):
def __init__(self, content='', creation_date=None, user_id=None, date=None):
self.content = content
self.creation_date = datetime.now() if creation_date is None else creation_date
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
self.user_id = user_id
self.date = self.creation_date if date is None else date
@ -123,7 +123,7 @@ class Subscriber(Base):
user = relationship('User', primaryjoin="User.id==Subscriber.user_id", cascade=None)
def __init__(self, thread_id=None, user_id=None, read=None):
def __init__(self, thread_id=None, user_id=None, read=False):
self.thread_id = thread_id
self.user_id = user_id
self.read = read

View File

@ -0,0 +1,35 @@
from datetime import timedelta, tzinfo, datetime
__author__ = 'tanshu'
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
# A UTC class.
class UTC(tzinfo):
"""UTC"""
def utcoffset(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return ZERO
utc = UTC()
def get_age(old_date):
now = datetime.utcnow().replace(tzinfo=utc)
delta = now - old_date
if delta.days > 0:
return '{0} days'.format(delta.days)
if delta.seconds > 3600:
return '{0} hours'.format(delta.seconds // 3600)
if delta.seconds > 60:
return '{0} minutes'.format(delta.seconds // 3600)
return '{0} seconds'.format(delta.seconds)

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# version 2013-05-30.1
# version 2013-06-06.1
CACHE:
/partial/404.html

View File

@ -18,17 +18,28 @@
</div>
<div class="widget-content nopadding">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
</th>
<th>
<h5>Details</h5>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in info">
<td><input type="checkbox" ng-model="item.selected"></td>
<td>
<span class="user-info">User: {{item.User}} at {{item.CreationDate | localTime}}</span>
<span class="user-info">User: {{item.User}} {{item.Age}} ago. Updated {{item.LastUpdated}} ago.</span>
<p><a ng-click="openMessage(item)">{{item.Title}}</a></p>
<p><a href="/Message/{{item.ThreadID}}">{{item.Title}}</a></p>
<a class="btn btn-mini" ng-repeat="tag in item.Tags"> {{tag}}</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</table>

View File

@ -4,7 +4,8 @@
<label for="txtTitle" class="control-label">Title</label>
<div class="controls">
<input type="text" id="txtTitle" class="input-block-level" ng-model="message.Title" ng-disabled="message.ThreadID"/>
<input type="text" id="txtTitle" class="input-block-level" ng-model="message.Title"
ng-disabled="message.ThreadID"/>
</div>
</div>
<div class="control-group">
@ -32,13 +33,30 @@
</label>
</div>
</div>
<div class="control-group">
<label for="txtUsers" class="control-label">Users</label>
<div class="controls">
<select id="txtUsers" data-placeholder="Users" multiple class="input-block-level chzn-select" chosen="users"
ng-model="message.Subscribers" ng-options="user for user in users">
</select>
</div>
</div>
<ul class="media-list">
<li class="media" ng-repeat="post in message.Posts" ng-disabled="post.PostID">
<div ng-switch="!$last" class="media-body">
<div ng-switch-when="true" class="media">
<div class="controls my-markdown">
<span class="md-title">Created on {{post.CreationDate}} by {{post.User}} (Dated: {{post.Date}})</span>
<div class="md-body" ng-bind-html-unsafe="post.Content | md"></div>
<div class="controls widget-box">
<div class="widget-title">
<h5>Created on {{post.CreationDate | localTime}} by {{post.User}} (Dated: {{post.Date}})</h5>
<ul class="nav nav-tabs pull-right">
<li>
<a href="#" ng-hide="!perms['Messages']"><b class="icon-edit"></b></a>
</li>
</ul>
</div>
<div class="widget-content" ng-bind-html-unsafe="post.Content | md"></div>
</div>
</div>
<div ng-switch-when="false" class="media">

View File

@ -297,6 +297,14 @@ overlord_directive.directive('chosen', ['$parse', function ($parse) {
createFunction = attrs['createFunction'],
options = {};
if (typeof createFunction !== 'undefined') {
options['create_option'] = function (data) {
var fn = $parse(attrs['createFunction'] + '("' + data + '")');
scope.$apply(function () {
fn(scope);
});
};
}
scope.$watch(list, function () {
element.trigger('liszt:updated');
}, true);
@ -306,16 +314,7 @@ overlord_directive.directive('chosen', ['$parse', function ($parse) {
element.trigger("liszt:updated");
}, true);
element.chosen({
create_option: function (data) {
if (typeof createFunction !== 'undefined') {
var fn = $parse(attrs['createFunction'] + '("' + data + '")');
scope.$apply(function () {
fn(scope);
});
}
}
});
element.chosen(options);
};
return {

View File

@ -51,7 +51,8 @@ overlord_service.factory('AccountType', ['$resource', function ($resource) {
overlord_service.factory('User', ['$resource', function ($resource) {
return $resource('/api/User/:id',
{id:'@UserID'}, {
query:{method:'GET', params:{list:true}, isArray:true}
query:{method:'GET', params:{list:true}, isArray:true},
names:{method:'GET', params:{names:true}, isArray:true}
});
}]);

View File

@ -10,7 +10,6 @@ var JournalCtrl = ['$scope', '$location', 'voucher', function ($scope, $location
return journals[i];
}
}
}
$scope.addJournal = function () {

View File

@ -1,9 +1,10 @@
'use strict';
var MessageCtrl = ['$scope', '$location', 'message', 'tags', 'priorities', function ($scope, $location, message, tags, priorities) {
var MessageCtrl = ['$scope', '$location', 'message', 'tags', 'users', 'priorities', function ($scope, $location, message, tags, users, priorities) {
$scope.message = message;
$scope.priorities = priorities
$scope.tags = tags;
$scope.users = users;
message.Posts.push({});
@ -52,6 +53,20 @@ MessageCtrl.resolve = {
Tag.query({}, successCb);
return deferred.promise;
}],
users: ['$q', 'User', function ($q, User) {
var deferred = $q.defer();
var successCb = function (result) {
var len = result.length;
var i = 0;
var list = []
for (i=0; i<len; i++){
list.push(result[i].Name);
}
deferred.resolve(list);
};
User.names({}, successCb);
return deferred.promise;
}],
priorities: ['$q', function ($q) {
var deferred = $q.defer();
deferred.resolve([

View File

@ -9,7 +9,6 @@ function PaymentCtrl($scope, $location, voucher, ledgers) {
return journals[i];
}
}
}
$scope.addJournal = function () {
@ -25,6 +24,7 @@ function PaymentCtrl($scope, $location, voucher, ledgers) {
}
delete $scope.ledger;
delete $scope.amount;
$('#txtLedger').val('');
$('#txtLedger').focus();
};

View File

@ -24,6 +24,7 @@ function ReceiptCtrl($scope, $routeParams, $location, voucher, ledgers, Voucher)
}
delete $scope.ledger;
delete $scope.amount;
$('#txtLedger').val('');
$('#txtLedger').focus();
};

View File

@ -1,115 +1,153 @@
import re
import uuid
from pyramid.response import Response
from pyramid.security import authenticated_userid
from pyramid.view import view_config
import transaction
from brewman import groupfinder
from brewman.models import DBSession
from brewman.models.auth import User, Group
from brewman.models.validation_exception import TryCatchFunction
from brewman.models.validation_exception import TryCatchFunction, ValidationError
@view_config(route_name='user_list', renderer='brewman:templates/angular_base.mako', permission='Users')
@view_config(request_method='GET', route_name='user_id', renderer='brewman:templates/angular_base.mako',
permission='Users')
@view_config(request_method='GET', route_name='user', renderer='brewman:templates/angular_base.mako',
permission='Users')
def html(request):
return {}
class view_user(object):
def __init__(self, request):
self.request = request
self.user = authenticated_userid(request)
self.permissions = None
self.HasPermission = False
if self.user is not None:
self.user = User.by_id(self.user)
self.HasPermission = 'User' in groupfinder(self.user.id, request)
@view_config(route_name='user_list', renderer='brewman:templates/angular_base.mako', permission='Users')
@view_config(request_method='GET', route_name='user_id', renderer='brewman:templates/angular_base.mako',
permission='Authenticated')
@view_config(request_method='GET', route_name='user', renderer='brewman:templates/angular_base.mako',
permission='Users')
def html(self):
return {}
@view_config(request_method='POST', route_name='api_user', renderer='json', permission='Users')
@TryCatchFunction
def save(request):
user = User(request.json_body['Name'], request.json_body['Password'], request.json_body['LockedOut'])
DBSession.add(user)
add_groups(user, request.json_body['Groups'])
transaction.commit()
return user_info(user.id)
@view_config(request_method='POST', route_name='api_user', renderer='json', permission='Users')
@TryCatchFunction
def save(self):
user = User(self.request.json_body['Name'], self.request.json_body['Password'],
self.request.json_body['LockedOut'])
DBSession.add(user)
self.add_groups(user, self.request.json_body['Groups'])
transaction.commit()
return self.user_info(user.id)
@view_config(request_method='POST', route_name='api_user_id', renderer='json', permission='Users')
@TryCatchFunction
def update(request):
id = request.matchdict['id']
p = re.compile('^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$')
if p.match(id):
user = User.by_id(uuid.UUID(id))
else:
user = User.by_name(id)
user.name = request.json_body['Name']
user.locked_out = request.json_body['LockedOut']
if request.json_body['Password'] != '' and request.json_body['Password'] != user.password:
user.password = request.json_body['Password']
add_groups(user, request.json_body['Groups'])
transaction.commit()
return user_info(user.id)
@view_config(request_method='POST', route_name='api_user_id', renderer='json', permission='Authenticated')
def update(self):
id = self.request.matchdict['id']
p = re.compile('^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$')
if p.match(id):
user = User.by_id(uuid.UUID(id))
else:
user = User.by_name(id)
return self.update_user(user)
@TryCatchFunction
def update_user(self, user):
if user is None:
raise ValidationError('User name / id not found')
if self.HasPermission:
user.name = self.request.json_body['Name']
user.locked_out = self.request.json_body['LockedOut']
self.add_groups(user, self.request.json_body['Groups'])
if self.request.json_body['Password'] != '' and self.request.json_body['Password'] != user.password:
user.password = self.request.json_body['Password']
transaction.commit()
return self.user_info(user.id)
def add_groups(user, groups):
for group in groups:
id = uuid.UUID(group['GroupID'])
ug = [g for g in user.groups if g.id == id]
ug = None if len(ug) == 0 else ug[0]
if group['Enabled'] and ug is None:
user.groups.append(Group.by_id(id))
elif not group['Enabled'] and ug:
user.groups.remove(ug)
@view_config(request_method='DELETE', route_name='api_user_id', renderer='json', permission='Users')
def delete(self):
id = self.request.matchdict.get('id', None)
if id is None:
response = Response("User is Null")
response.status_int = 500
return response
else:
response = Response("User deletion not implemented")
response.status_int = 500
return response
@view_config(request_method='DELETE', route_name='api_user_id', renderer='json', permission='Users')
def delete(request):
id = request.matchdict.get('id', None)
if id is None:
response = Response("User is Null")
response.status_int = 500
return response
else:
response = Response("User deletion not implemented")
response.status_int = 500
return response
@view_config(request_method='GET', route_name='api_user_id', renderer='json', permission='Authenticated')
def show_id(self):
id = self.request.matchdict['id']
p = re.compile('^[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}$')
if p.match(id):
id = uuid.UUID(id)
return self.user_info(id)
@view_config(request_method='GET', route_name='api_user_id', renderer='json', permission='Users')
def show_id(request):
id = request.matchdict['id']
p = re.compile('^[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}$')
if p.match(id):
id = uuid.UUID(id)
return user_info(id)
@view_config(request_method='GET', route_name='api_user', renderer='json', permission='Users')
def show_blank(self):
return self.user_info(None)
@view_config(request_method='GET', route_name='api_user', renderer='json', permission='Users')
def show_blank(request):
return user_info(None)
@view_config(request_method='GET', route_name='api_user', renderer='json', request_param='list', permission='Users')
def show_list(self):
list = User.list()
users = []
for item in list:
user = {'Name': item.name, 'LockedOut': item.locked_out,
'Groups': [], 'Url': self.request.route_url('user_id', id=item.name)}
for group in item.groups:
user['Groups'].append(group.name)
users.append(user)
return users
@view_config(request_method='GET', route_name='api_user', renderer='json', request_param='list', permission='Users')
def show_list(request):
list = User.list()
users = []
for item in list:
user = {'Name': item.name, 'LockedOut': item.locked_out,
'Groups': [], 'Url': request.route_url('user_id', id=item.name)}
for group in item.groups:
user['Groups'].append(group.name)
users.append(user)
return users
@view_config(request_method='GET', route_name='api_user', renderer='json', request_param='names', permission='Authenticated')
def show_name(self):
list = User.query().filter(User.locked_out == False).order_by(User.name).all()
users = [{'Name': item.name} for item in list]
# for item in list:
# users.append({'Name': item.name})
return users
def user_info(id):
if id is None:
account = {'Name': '', 'LockedOut': False, 'Groups': []}
for item in Group.list():
account['Groups'].append({'GroupID': item.id, 'Name': item.name, 'Enabled': False})
else:
def user_info(self, id):
if id is None:
account = {'Name': '', 'LockedOut': False, 'Groups': []}
for item in Group.list():
account['Groups'].append({'GroupID': item.id, 'Name': item.name, 'Enabled': False})
return account
if isinstance(id, uuid.UUID):
user = User.by_id(id)
else:
user = User.by_name(id)
account = {'UserID': user.id, 'Name': user.name, 'Password': '', 'LockedOut': user.locked_out, 'Groups': []}
for item in Group.list():
account['Groups'].append(
{'GroupID': item.id, 'Name': item.name, 'Enabled': True if item in user.groups else False})
return account
if self.HasPermission:
account = {'UserID': user.id, 'Name': user.name, 'Password': '', 'LockedOut': user.locked_out, 'Groups': []}
for item in Group.list():
account['Groups'].append(
{'GroupID': item.id, 'Name': item.name, 'Enabled': True if item in user.groups else False})
elif self.user.id == user.id:
account = {'UserID': user.id, 'Name': user.name, 'Password': '', 'LockedOut': user.locked_out, 'Groups': []}
else:
response = Response("User can only update his/her password")
response.status_int = 500
return response
return account
def add_groups(self, user, groups):
for group in groups:
id = uuid.UUID(group['GroupID'])
ug = [g for g in user.groups if g.id == id]
ug = None if len(ug) == 0 else ug[0]
if group['Enabled'] and ug is None:
user.groups.append(Group.by_id(id))
elif not group['Enabled'] and ug:
user.groups.remove(ug)

View File

@ -3,10 +3,13 @@ import uuid
from pyramid.security import authenticated_userid
from pyramid.view import view_config
from sqlalchemy.orm import joinedload_all
import transaction
from brewman.models import DBSession
from brewman.models.auth import User
from brewman.models.messaging import Thread, Post, Tag
from brewman.models.messaging import Thread, Post, Tag, Subscriber
from brewman.models.tzinfoutc import get_age
from brewman.models.validation_exception import TryCatchFunction
@ -35,6 +38,10 @@ def save(request):
tag = Tag(name=name)
DBSession.add(tag)
thread.tags.append(tag)
for item in request.json_body['Subscribers']:
subscriber = Subscriber(user_id=User.by_name(item).id, read=False)
thread.subscribers.append(subscriber)
DBSession.add(subscriber)
transaction.commit()
return thread_info(thread.id)
@ -74,13 +81,17 @@ def update(request):
post = Post(content=item['Content'].strip(), date=dt, user_id=user_id)
thread.posts.append(post)
DBSession.add(post)
for subscriber in thread.subscribers:
subscriber.read = False
transaction.commit()
return thread_info(thread.id)
@view_config(request_method='GET', route_name='api_message', renderer='json', permission='Authenticated')
def show_blank(request):
return thread_info(None)
user = User.by_id(authenticated_userid(request)).name
return {'Title': '', 'User': user, 'Tags': [], 'Priority': 4, 'Public': False,
'Posts': [], 'Closed': False, 'Subscribers': [user]}
@view_config(request_method='GET', route_name='api_message_id', renderer='json', permission='Authenticated')
@ -91,43 +102,51 @@ def show_id(request):
@view_config(request_method='GET', route_name='api_message', renderer='json', request_param='list')
def show_list(request):
list = Thread.query().filter(Thread.closed == False)
# Thread.subscribers, Thread.posts,
if authenticated_userid(request) is None:
list = list.filter(Thread.public == True)
# else:
# list.filter(Thread.subscribers.any(Subscriber.user_id == uuid.UUID(authenticated_userid(request))))
list = list.order_by(Thread.priority).all()
tags = {}
threads = []
for item in list:
thread = {'ThreadID': item.id, 'Title': item.title,
thread = {'ThreadID': item.id, 'Title': item.title, 'Age': get_age(item.creation_date),
'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'User': item.user.name,
'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': []}
'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': [], 'Subscribers': []}
for tag in item.tags:
thread['Tags'].append(tag.name)
if not tag.name in tags:
tags[tag.name] = 1
else:
tags[tag.name] += 1
for subscriber in item.subscribers:
thread['Subscribers'].append(subscriber.user.name)
last_updated = item.creation_date
for post in item.posts:
thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name,
'Date': post.date.strftime('%d-%b-%Y %H:%M'),
'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
if post.creation_date > last_updated:
last_updated = post.creation_date
# thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name,
# 'Date': post.date.strftime('%d-%b-%Y %H:%M'),
# 'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
threads.append(thread)
thread['LastUpdated'] = get_age(last_updated)
return {'Tags': tags, 'Threads': threads}
def thread_info(id):
if id is None:
thread = {'Title': '', 'Tags': [], 'Priority': 4, 'Public': False, 'Posts': [], 'Closed': False}
else:
item = Thread.by_id(id)
thread = {'ThreadID': item.id, 'Title': item.title, 'Closed': item.closed,
'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'User': item.user.name,
'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': []}
for tag in item.tags:
thread['Tags'].append(tag.name)
for post in item.posts:
thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name,
'Date': post.date.strftime('%d-%b-%Y %H:%M'),
'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
item = Thread.by_id(id)
thread = {'ThreadID': item.id, 'Title': item.title, 'Closed': item.closed,
'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'User': item.user.name,
'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': [], 'Subscribers': []}
for tag in item.tags:
thread['Tags'].append(tag.name)
for subscriber in item.subscribers:
thread['Subscribers'].append(subscriber.user.name)
for post in item.posts:
thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name,
'Date': post.date.strftime('%d-%b-%Y'),
'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
return thread