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): def list(cls):
return DBSession.query(cls).order_by(cls.name).all() return DBSession.query(cls).order_by(cls.name).all()
@classmethod
def query(cls):
return DBSession.query(cls)
@classmethod @classmethod
def filtered_list(cls, name): def filtered_list(cls, name):
query = DBSession.query(cls) 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): def __init__(self, title=None, creation_date=None, user_id=None, priority=5, public=False, closed=False):
self.title = title 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.user_id = user_id
self.priority = priority self.priority = priority
self.public = public self.public = public
@ -96,7 +96,7 @@ class Post(Base):
def __init__(self, content='', creation_date=None, user_id=None, date=None): def __init__(self, content='', creation_date=None, user_id=None, date=None):
self.content = content 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.user_id = user_id
self.date = self.creation_date if date is None else date 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) 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.thread_id = thread_id
self.user_id = user_id self.user_id = user_id
self.read = read 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 CACHE MANIFEST
# version 2013-05-30.1 # version 2013-06-06.1
CACHE: CACHE:
/partial/404.html /partial/404.html

View File

@ -18,17 +18,28 @@
</div> </div>
<div class="widget-content nopadding"> <div class="widget-content nopadding">
<table class="table table-bordered table-striped"> <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> <tbody>
<tr ng-repeat="item in info"> <tr ng-repeat="item in info">
<td><input type="checkbox" ng-model="item.selected"></td> <td><input type="checkbox" ng-model="item.selected"></td>
<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> <a class="btn btn-mini" ng-repeat="tag in item.Tags"> {{tag}}</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table>
</div> </div>
</div> </div>
</table> </table>

View File

@ -4,7 +4,8 @@
<label for="txtTitle" class="control-label">Title</label> <label for="txtTitle" class="control-label">Title</label>
<div class="controls"> <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> </div>
<div class="control-group"> <div class="control-group">
@ -32,13 +33,30 @@
</label> </label>
</div> </div>
</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"> <ul class="media-list">
<li class="media" ng-repeat="post in message.Posts" ng-disabled="post.PostID"> <li class="media" ng-repeat="post in message.Posts" ng-disabled="post.PostID">
<div ng-switch="!$last" class="media-body"> <div ng-switch="!$last" class="media-body">
<div ng-switch-when="true" class="media"> <div ng-switch-when="true" class="media">
<div class="controls my-markdown"> <div class="controls widget-box">
<span class="md-title">Created on {{post.CreationDate}} by {{post.User}} (Dated: {{post.Date}})</span> <div class="widget-title">
<div class="md-body" ng-bind-html-unsafe="post.Content | md"></div> <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> </div>
<div ng-switch-when="false" class="media"> <div ng-switch-when="false" class="media">

View File

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

View File

@ -51,7 +51,8 @@ overlord_service.factory('AccountType', ['$resource', function ($resource) {
overlord_service.factory('User', ['$resource', function ($resource) { overlord_service.factory('User', ['$resource', function ($resource) {
return $resource('/api/User/:id', return $resource('/api/User/:id',
{id:'@UserID'}, { {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]; return journals[i];
} }
} }
} }
$scope.addJournal = function () { $scope.addJournal = function () {

View File

@ -1,9 +1,10 @@
'use strict'; '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.message = message;
$scope.priorities = priorities $scope.priorities = priorities
$scope.tags = tags; $scope.tags = tags;
$scope.users = users;
message.Posts.push({}); message.Posts.push({});
@ -52,6 +53,20 @@ MessageCtrl.resolve = {
Tag.query({}, successCb); Tag.query({}, successCb);
return deferred.promise; 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) { priorities: ['$q', function ($q) {
var deferred = $q.defer(); var deferred = $q.defer();
deferred.resolve([ deferred.resolve([

View File

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

View File

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

View File

@ -1,115 +1,153 @@
import re import re
import uuid import uuid
from pyramid.response import Response from pyramid.response import Response
from pyramid.security import authenticated_userid
from pyramid.view import view_config from pyramid.view import view_config
import transaction import transaction
from brewman import groupfinder
from brewman.models import DBSession from brewman.models import DBSession
from brewman.models.auth import User, Group 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') class view_user(object):
@view_config(request_method='GET', route_name='user_id', renderer='brewman:templates/angular_base.mako', def __init__(self, request):
permission='Users') self.request = request
@view_config(request_method='GET', route_name='user', renderer='brewman:templates/angular_base.mako', self.user = authenticated_userid(request)
permission='Users') self.permissions = None
def html(request): self.HasPermission = False
return {} 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') @view_config(request_method='POST', route_name='api_user', renderer='json', permission='Users')
@TryCatchFunction @TryCatchFunction
def save(request): def save(self):
user = User(request.json_body['Name'], request.json_body['Password'], request.json_body['LockedOut']) user = User(self.request.json_body['Name'], self.request.json_body['Password'],
DBSession.add(user) self.request.json_body['LockedOut'])
add_groups(user, request.json_body['Groups']) DBSession.add(user)
transaction.commit() self.add_groups(user, self.request.json_body['Groups'])
return user_info(user.id) transaction.commit()
return self.user_info(user.id)
@view_config(request_method='POST', route_name='api_user_id', renderer='json', permission='Users') @view_config(request_method='POST', route_name='api_user_id', renderer='json', permission='Authenticated')
@TryCatchFunction def update(self):
def update(request): id = self.request.matchdict['id']
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}$')
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):
if p.match(id): user = User.by_id(uuid.UUID(id))
user = User.by_id(uuid.UUID(id)) else:
else: user = User.by_name(id)
user = User.by_name(id) return self.update_user(user)
user.name = request.json_body['Name']
user.locked_out = request.json_body['LockedOut'] @TryCatchFunction
if request.json_body['Password'] != '' and request.json_body['Password'] != user.password: def update_user(self, user):
user.password = request.json_body['Password'] if user is None:
add_groups(user, request.json_body['Groups']) raise ValidationError('User name / id not found')
transaction.commit() if self.HasPermission:
return user_info(user.id) 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): @view_config(request_method='DELETE', route_name='api_user_id', renderer='json', permission='Users')
for group in groups: def delete(self):
id = uuid.UUID(group['GroupID']) id = self.request.matchdict.get('id', None)
ug = [g for g in user.groups if g.id == id] if id is None:
ug = None if len(ug) == 0 else ug[0] response = Response("User is Null")
if group['Enabled'] and ug is None: response.status_int = 500
user.groups.append(Group.by_id(id)) return response
elif not group['Enabled'] and ug: else:
user.groups.remove(ug) 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') @view_config(request_method='GET', route_name='api_user_id', renderer='json', permission='Authenticated')
def delete(request): def show_id(self):
id = request.matchdict.get('id', None) id = self.request.matchdict['id']
if id is None: 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}$')
response = Response("User is Null") if p.match(id):
response.status_int = 500 id = uuid.UUID(id)
return response return self.user_info(id)
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='Users') @view_config(request_method='GET', route_name='api_user', renderer='json', permission='Users')
def show_id(request): def show_blank(self):
id = request.matchdict['id'] return self.user_info(None)
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') @view_config(request_method='GET', route_name='api_user', renderer='json', request_param='list', permission='Users')
def show_blank(request): def show_list(self):
return user_info(None) 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') @view_config(request_method='GET', route_name='api_user', renderer='json', request_param='names', permission='Authenticated')
def show_list(request): def show_name(self):
list = User.list() list = User.query().filter(User.locked_out == False).order_by(User.name).all()
users = [] users = [{'Name': item.name} for item in list]
for item in list: # for item in list:
user = {'Name': item.name, 'LockedOut': item.locked_out, # users.append({'Name': item.name})
'Groups': [], 'Url': request.route_url('user_id', id=item.name)} return users
for group in item.groups:
user['Groups'].append(group.name)
users.append(user)
return users
def user_info(id): def user_info(self, id):
if id is None: if id is None:
account = {'Name': '', 'LockedOut': False, 'Groups': []} account = {'Name': '', 'LockedOut': False, 'Groups': []}
for item in Group.list(): for item in Group.list():
account['Groups'].append({'GroupID': item.id, 'Name': item.name, 'Enabled': False}) account['Groups'].append({'GroupID': item.id, 'Name': item.name, 'Enabled': False})
else: return account
if isinstance(id, uuid.UUID): if isinstance(id, uuid.UUID):
user = User.by_id(id) user = User.by_id(id)
else: else:
user = User.by_name(id) user = User.by_name(id)
account = {'UserID': user.id, 'Name': user.name, 'Password': '', 'LockedOut': user.locked_out, 'Groups': []}
for item in Group.list(): if self.HasPermission:
account['Groups'].append( account = {'UserID': user.id, 'Name': user.name, 'Password': '', 'LockedOut': user.locked_out, 'Groups': []}
{'GroupID': item.id, 'Name': item.name, 'Enabled': True if item in user.groups else False}) for item in Group.list():
return account 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.security import authenticated_userid
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy.orm import joinedload_all
import transaction import transaction
from brewman.models import DBSession 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 from brewman.models.validation_exception import TryCatchFunction
@ -35,6 +38,10 @@ def save(request):
tag = Tag(name=name) tag = Tag(name=name)
DBSession.add(tag) DBSession.add(tag)
thread.tags.append(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() transaction.commit()
return thread_info(thread.id) return thread_info(thread.id)
@ -74,13 +81,17 @@ def update(request):
post = Post(content=item['Content'].strip(), date=dt, user_id=user_id) post = Post(content=item['Content'].strip(), date=dt, user_id=user_id)
thread.posts.append(post) thread.posts.append(post)
DBSession.add(post) DBSession.add(post)
for subscriber in thread.subscribers:
subscriber.read = False
transaction.commit() transaction.commit()
return thread_info(thread.id) return thread_info(thread.id)
@view_config(request_method='GET', route_name='api_message', renderer='json', permission='Authenticated') @view_config(request_method='GET', route_name='api_message', renderer='json', permission='Authenticated')
def show_blank(request): 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') @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') @view_config(request_method='GET', route_name='api_message', renderer='json', request_param='list')
def show_list(request): def show_list(request):
list = Thread.query().filter(Thread.closed == False) list = Thread.query().filter(Thread.closed == False)
# Thread.subscribers, Thread.posts,
if authenticated_userid(request) is None: if authenticated_userid(request) is None:
list = list.filter(Thread.public == True) 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() list = list.order_by(Thread.priority).all()
tags = {} tags = {}
threads = [] threads = []
for item in list: 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, '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: for tag in item.tags:
thread['Tags'].append(tag.name) thread['Tags'].append(tag.name)
if not tag.name in tags: if not tag.name in tags:
tags[tag.name] = 1 tags[tag.name] = 1
else: else:
tags[tag.name] += 1 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: for post in item.posts:
thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name, if post.creation_date > last_updated:
'Date': post.date.strftime('%d-%b-%Y %H:%M'), last_updated = post.creation_date
'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')}) # 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) threads.append(thread)
thread['LastUpdated'] = get_age(last_updated)
return {'Tags': tags, 'Threads': threads} return {'Tags': tags, 'Threads': threads}
def thread_info(id): def thread_info(id):
if id is None: item = Thread.by_id(id)
thread = {'Title': '', 'Tags': [], 'Priority': 4, 'Public': False, 'Posts': [], 'Closed': False} thread = {'ThreadID': item.id, 'Title': item.title, 'Closed': item.closed,
else: 'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'User': item.user.name,
item = Thread.by_id(id) 'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': [], 'Subscribers': []}
thread = {'ThreadID': item.id, 'Title': item.title, 'Closed': item.closed, for tag in item.tags:
'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'User': item.user.name, thread['Tags'].append(tag.name)
'Priority': item.priority, 'Public': item.public, 'Tags': [], 'Posts': []} for subscriber in item.subscribers:
for tag in item.tags: thread['Subscribers'].append(subscriber.user.name)
thread['Tags'].append(tag.name) for post in item.posts:
for post in item.posts: thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name,
thread['Posts'].append({'PostID': post.id, 'Content': post.content, 'User': post.user.name, 'Date': post.date.strftime('%d-%b-%Y'),
'Date': post.date.strftime('%d-%b-%Y %H:%M'), 'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
'CreationDate': post.creation_date.strftime('%d-%b-%Y %H:%M')})
return thread return thread