|
a/ForgeTracker/forgetracker/model/ticket.py |
|
b/ForgeTracker/forgetracker/model/ticket.py |
|
... |
|
... |
71 |
session = project_orm_session
|
71 |
session = project_orm_session
|
72 |
indexes = [ 'app_config_id' ]
|
72 |
indexes = [ 'app_config_id' ]
|
73 |
|
73 |
|
74 |
type_s = 'Globals'
|
74 |
type_s = 'Globals'
|
75 |
_id = FieldProperty(schema.ObjectId)
|
75 |
_id = FieldProperty(schema.ObjectId)
|
76 |
app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
|
76 |
app_config_id = ForeignIdProperty(AppConfig, if_missing=lambda:c.app.config._id)
|
|
|
77 |
app_config = RelationProperty(AppConfig, via='app_config_id')
|
77 |
last_ticket_num = FieldProperty(int)
|
78 |
last_ticket_num = FieldProperty(int)
|
78 |
status_names = FieldProperty(str)
|
79 |
status_names = FieldProperty(str)
|
79 |
open_status_names = FieldProperty(str)
|
80 |
open_status_names = FieldProperty(str)
|
80 |
closed_status_names = FieldProperty(str)
|
81 |
closed_status_names = FieldProperty(str)
|
81 |
milestone_names = FieldProperty(str, if_missing='')
|
82 |
milestone_names = FieldProperty(str, if_missing='')
|
|
... |
|
... |
95 |
'created_date': True,
|
96 |
'created_date': True,
|
96 |
'mod_date': True,
|
97 |
'mod_date': True,
|
97 |
'labels': False,
|
98 |
'labels': False,
|
98 |
})
|
99 |
})
|
99 |
|
100 |
|
100 |
@classmethod
|
|
|
101 |
def next_ticket_num(cls):
|
101 |
def next_ticket_num(self):
|
102 |
gbl = cls.query.find_and_modify(
|
102 |
gbl = Globals.query.find_and_modify(
|
103 |
query=dict(app_config_id=c.app.config._id),
|
103 |
query=dict(app_config_id=self.app_config_id),
|
104 |
update={'$inc': { 'last_ticket_num': 1}},
|
104 |
update={'$inc': { 'last_ticket_num': 1}},
|
105 |
new=True)
|
105 |
new=True)
|
106 |
session(cls).expunge(gbl)
|
106 |
session(gbl).expunge(gbl)
|
107 |
return gbl.last_ticket_num
|
107 |
return gbl.last_ticket_num
|
108 |
|
108 |
|
109 |
@property
|
109 |
@property
|
110 |
def all_status_names(self):
|
110 |
def all_status_names(self):
|
111 |
return ' '.join([self.open_status_names, self.closed_status_names])
|
111 |
return ' '.join([self.open_status_names, self.closed_status_names])
|
|
... |
|
... |
187 |
d = dict(name=name, hits=0, closed=0)
|
187 |
d = dict(name=name, hits=0, closed=0)
|
188 |
if not (fld_name and m_name):
|
188 |
if not (fld_name and m_name):
|
189 |
return d
|
189 |
return d
|
190 |
mongo_query = {'custom_fields.%s' % fld_name: m_name}
|
190 |
mongo_query = {'custom_fields.%s' % fld_name: m_name}
|
191 |
r = Ticket.query.find(dict(
|
191 |
r = Ticket.query.find(dict(
|
192 |
mongo_query, app_config_id=c.app.config._id, deleted=False))
|
192 |
mongo_query, app_config_id=self.app_config_id, deleted=False))
|
193 |
tickets = [t for t in r if security.has_access(t, 'read')]
|
193 |
tickets = [t for t in r if security.has_access(t, 'read')]
|
194 |
d['hits'] = len(tickets)
|
194 |
d['hits'] = len(tickets)
|
195 |
d['closed'] = sum(1 for t in tickets
|
195 |
d['closed'] = sum(1 for t in tickets
|
196 |
if t.status in c.app.globals.set_of_closed_status_names)
|
196 |
if t.status in self.set_of_closed_status_names)
|
197 |
return d
|
197 |
return d
|
198 |
|
198 |
|
199 |
def invalidate_bin_counts(self):
|
199 |
def invalidate_bin_counts(self):
|
200 |
'''Force expiry of bin counts and queue them to be updated.'''
|
200 |
'''Force expiry of bin counts and queue them to be updated.'''
|
201 |
# To prevent multiple calls to this method from piling on redundant
|
201 |
# To prevent multiple calls to this method from piling on redundant
|
|
... |
|
... |
227 |
for field in self.custom_fields
|
227 |
for field in self.custom_fields
|
228 |
if field.get('show_in_search')]
|
228 |
if field.get('show_in_search')]
|
229 |
|
229 |
|
230 |
def has_deleted_tickets(self):
|
230 |
def has_deleted_tickets(self):
|
231 |
return Ticket.query.find(dict(
|
231 |
return Ticket.query.find(dict(
|
232 |
app_config_id=c.app.config._id, deleted=True)).count() > 0
|
232 |
app_config_id=self.app_config_id, deleted=True)).count() > 0
|
233 |
|
233 |
|
234 |
def move_tickets(self, ticket_ids, destination_tracker_id):
|
234 |
def move_tickets(self, ticket_ids, destination_tracker_id):
|
235 |
tracker = AppConfig.query.get(_id=destination_tracker_id)
|
235 |
tracker = AppConfig.query.get(_id=destination_tracker_id)
|
236 |
tickets = Ticket.query.find(dict(
|
236 |
tickets = Ticket.query.find(dict(
|
237 |
_id={'$in': [ObjectId(id) for id in ticket_ids]},
|
237 |
_id={'$in': [ObjectId(id) for id in ticket_ids]},
|
238 |
app_config_id=c.app.config._id)).sort('ticket_num').all()
|
238 |
app_config_id=self.app_config_id)).sort('ticket_num').all()
|
239 |
filtered = self.filtered_by_subscription({t._id: t for t in tickets})
|
239 |
filtered = self.filtered_by_subscription({t._id: t for t in tickets})
|
240 |
original_ticket_nums = {t._id: t.ticket_num for t in tickets}
|
240 |
original_ticket_nums = {t._id: t.ticket_num for t in tickets}
|
241 |
users = User.query.find({'_id': {'$in': filtered.keys()}}).all()
|
241 |
users = User.query.find({'_id': {'$in': filtered.keys()}}).all()
|
242 |
moved_tickets = {}
|
242 |
moved_tickets = {}
|
243 |
for ticket in tickets:
|
243 |
for ticket in tickets:
|
|
... |
|
... |
245 |
moved_tickets[moved._id] = moved
|
245 |
moved_tickets[moved._id] = moved
|
246 |
mail = dict(
|
246 |
mail = dict(
|
247 |
fromaddr = str(c.user.email_address_header()),
|
247 |
fromaddr = str(c.user.email_address_header()),
|
248 |
reply_to = str(c.user.email_address_header()),
|
248 |
reply_to = str(c.user.email_address_header()),
|
249 |
subject = '[%s:%s] Mass ticket moving by %s' % (c.project.shortname,
|
249 |
subject = '[%s:%s] Mass ticket moving by %s' % (c.project.shortname,
|
250 |
c.app.config.options.mount_point,
|
250 |
self.app_config.options.mount_point,
|
251 |
c.user.display_name))
|
251 |
c.user.display_name))
|
252 |
tmpl = g.jinja2_env.get_template('forgetracker:data/mass_move_report.html')
|
252 |
tmpl = g.jinja2_env.get_template('forgetracker:data/mass_move_report.html')
|
253 |
|
253 |
|
254 |
tmpl_context = {
|
254 |
tmpl_context = {
|
255 |
'original_tracker': '%s:%s' % (c.project.shortname,
|
255 |
'original_tracker': '%s:%s' % (c.project.shortname,
|
256 |
c.app.config.options.mount_point),
|
256 |
self.app_config.options.mount_point),
|
257 |
'destination_tracker': '%s:%s' % (tracker.project.shortname,
|
257 |
'destination_tracker': '%s:%s' % (tracker.project.shortname,
|
258 |
tracker.options.mount_point),
|
258 |
tracker.options.mount_point),
|
259 |
'tickets': [],
|
259 |
'tickets': [],
|
260 |
}
|
260 |
}
|
261 |
for user in users:
|
261 |
for user in users:
|
|
... |
|
... |
268 |
message_id = h.gen_message_id(),
|
268 |
message_id = h.gen_message_id(),
|
269 |
text = tmpl.render(tmpl_context),
|
269 |
text = tmpl.render(tmpl_context),
|
270 |
destinations = [str(user._id)]))
|
270 |
destinations = [str(user._id)]))
|
271 |
mail_tasks.sendmail.post(**mail)
|
271 |
mail_tasks.sendmail.post(**mail)
|
272 |
|
272 |
|
273 |
if c.app.config.options.get('TicketMonitoringType') in (
|
273 |
if self.app_config.options.get('TicketMonitoringType') in (
|
274 |
'AllTicketChanges', 'AllPublicTicketChanges'):
|
274 |
'AllTicketChanges', 'AllPublicTicketChanges'):
|
275 |
monitoring_email = c.app.config.options.get('TicketMonitoringEmail')
|
275 |
monitoring_email = self.app_config.options.get('TicketMonitoringEmail')
|
276 |
tmpl_context['tickets'] = [{
|
276 |
tmpl_context['tickets'] = [{
|
277 |
'original_num': original_ticket_nums[_id],
|
277 |
'original_num': original_ticket_nums[_id],
|
278 |
'destination_num': moved_tickets[_id].ticket_num,
|
278 |
'destination_num': moved_tickets[_id].ticket_num,
|
279 |
'summary': moved_tickets[_id].summary
|
279 |
'summary': moved_tickets[_id].summary
|
280 |
} for _id, t in moved_tickets.iteritems()
|
280 |
} for _id, t in moved_tickets.iteritems()
|
281 |
if (not t.private or
|
281 |
if (not t.private or
|
282 |
c.app.config.options.get('TicketMonitoringType') ==
|
282 |
self.app_config.options.get('TicketMonitoringType') ==
|
283 |
'AllTicketChanges')]
|
283 |
'AllTicketChanges')]
|
284 |
if len(tmpl_context['tickets']) > 0:
|
284 |
if len(tmpl_context['tickets']) > 0:
|
285 |
mail.update(dict(
|
285 |
mail.update(dict(
|
286 |
message_id = h.gen_message_id(),
|
286 |
message_id = h.gen_message_id(),
|
287 |
text = tmpl.render(tmpl_context),
|
287 |
text = tmpl.render(tmpl_context),
|
288 |
destinations = [monitoring_email]))
|
288 |
destinations = [monitoring_email]))
|
289 |
mail_tasks.sendmail.post(**mail)
|
289 |
mail_tasks.sendmail.post(**mail)
|
290 |
|
290 |
|
291 |
moved_from = '%s/%s' % (c.project.shortname, c.app.config.options.mount_point)
|
291 |
moved_from = '%s/%s' % (c.project.shortname, self.app_config.options.mount_point)
|
292 |
moved_to = '%s/%s' % (tracker.project.shortname, tracker.options.mount_point)
|
292 |
moved_to = '%s/%s' % (tracker.project.shortname, tracker.options.mount_point)
|
293 |
text = 'Tickets moved from %s to %s' % (moved_from, moved_to)
|
293 |
text = 'Tickets moved from %s to %s' % (moved_from, moved_to)
|
294 |
Notification.post_user(c.user, None, 'flash', text=text)
|
294 |
Notification.post_user(c.user, None, 'flash', text=text)
|
295 |
|
295 |
|
296 |
def update_tickets(self, **post_data):
|
296 |
def update_tickets(self, **post_data):
|
297 |
from forgetracker.tracker_main import get_change_text, get_label
|
297 |
from forgetracker.tracker_main import get_change_text, get_label
|
298 |
tickets = Ticket.query.find(dict(
|
298 |
tickets = Ticket.query.find(dict(
|
299 |
_id={'$in':[ObjectId(id) for id in aslist(post_data['__ticket_ids'])]},
|
299 |
_id={'$in':[ObjectId(id) for id in aslist(post_data['__ticket_ids'])]},
|
300 |
app_config_id=c.app.config._id)).all()
|
300 |
app_config_id=self.app_config_id)).all()
|
301 |
|
301 |
|
302 |
fields = set(['status'])
|
302 |
fields = set(['status'])
|
303 |
values = {}
|
303 |
values = {}
|
304 |
for k in fields:
|
304 |
for k in fields:
|
305 |
v = post_data.get(k)
|
305 |
v = post_data.get(k)
|
|
... |
|
... |
312 |
if user:
|
312 |
if user:
|
313 |
values['assigned_to_id'] = user._id
|
313 |
values['assigned_to_id'] = user._id
|
314 |
|
314 |
|
315 |
custom_values = {}
|
315 |
custom_values = {}
|
316 |
custom_fields = {}
|
316 |
custom_fields = {}
|
317 |
for cf in c.app.globals.custom_fields or []:
|
317 |
for cf in self.custom_fields or []:
|
318 |
v = post_data.get(cf.name)
|
318 |
v = post_data.get(cf.name)
|
319 |
if v:
|
319 |
if v:
|
320 |
custom_values[cf.name] = v
|
320 |
custom_values[cf.name] = v
|
321 |
custom_fields[cf.name] = cf
|
321 |
custom_fields[cf.name] = cf
|
322 |
|
322 |
|
|
... |
|
... |
367 |
yield (changed_tickets[t_id], jinja2.Markup(changes[t_id]))
|
367 |
yield (changed_tickets[t_id], jinja2.Markup(changes[t_id]))
|
368 |
mail = dict(
|
368 |
mail = dict(
|
369 |
fromaddr = str(c.user._id),
|
369 |
fromaddr = str(c.user._id),
|
370 |
reply_to = str(c.user._id),
|
370 |
reply_to = str(c.user._id),
|
371 |
subject = '[%s:%s] Mass edit changes by %s' % (c.project.shortname,
|
371 |
subject = '[%s:%s] Mass edit changes by %s' % (c.project.shortname,
|
372 |
c.app.config.options.mount_point,
|
372 |
self.app_config.options.mount_point,
|
373 |
c.user.display_name),
|
373 |
c.user.display_name),
|
374 |
)
|
374 |
)
|
375 |
tmpl = g.jinja2_env.get_template('forgetracker:data/mass_report.html')
|
375 |
tmpl = g.jinja2_env.get_template('forgetracker:data/mass_report.html')
|
376 |
head = []
|
376 |
head = []
|
377 |
for f, v in sorted(values.iteritems()):
|
377 |
for f, v in sorted(values.iteritems()):
|
|
... |
|
... |
392 |
message_id = h.gen_message_id(),
|
392 |
message_id = h.gen_message_id(),
|
393 |
text = tmpl.render(tmpl_context),
|
393 |
text = tmpl.render(tmpl_context),
|
394 |
destinations = [str(user._id)]))
|
394 |
destinations = [str(user._id)]))
|
395 |
mail_tasks.sendmail.post(**mail)
|
395 |
mail_tasks.sendmail.post(**mail)
|
396 |
|
396 |
|
397 |
if c.app.config.options.get('TicketMonitoringType') in (
|
397 |
if self.app_config.options.get('TicketMonitoringType') in (
|
398 |
'AllTicketChanges', 'AllPublicTicketChanges'):
|
398 |
'AllTicketChanges', 'AllPublicTicketChanges'):
|
399 |
monitoring_email = c.app.config.options.get('TicketMonitoringEmail')
|
399 |
monitoring_email = self.app_config.options.get('TicketMonitoringEmail')
|
400 |
visible_changes = []
|
400 |
visible_changes = []
|
401 |
for t_id, t in changed_tickets.items():
|
401 |
for t_id, t in changed_tickets.items():
|
402 |
if (not t.private or
|
402 |
if (not t.private or
|
403 |
c.app.config.options.get('TicketMonitoringType') ==
|
403 |
self.app_config.options.get('TicketMonitoringType') ==
|
404 |
'AllTicketChanges'):
|
404 |
'AllTicketChanges'):
|
405 |
visible_changes.append(
|
405 |
visible_changes.append(
|
406 |
(changed_tickets[t_id], jinja2.Markup(changes[t_id])))
|
406 |
(changed_tickets[t_id], jinja2.Markup(changes[t_id])))
|
407 |
if visible_changes:
|
407 |
if visible_changes:
|
408 |
tmpl_context['data'].update({'changes': visible_changes})
|
408 |
tmpl_context['data'].update({'changes': visible_changes})
|
|
... |
|
... |
410 |
message_id = h.gen_message_id(),
|
410 |
message_id = h.gen_message_id(),
|
411 |
text = tmpl.render(tmpl_context),
|
411 |
text = tmpl.render(tmpl_context),
|
412 |
destinations = [monitoring_email]))
|
412 |
destinations = [monitoring_email]))
|
413 |
mail_tasks.sendmail.post(**mail)
|
413 |
mail_tasks.sendmail.post(**mail)
|
414 |
|
414 |
|
415 |
c.app.globals.invalidate_bin_counts()
|
415 |
self.invalidate_bin_counts()
|
416 |
ThreadLocalORMSession.flush_all()
|
416 |
ThreadLocalORMSession.flush_all()
|
417 |
app = '%s/%s' % (c.project.shortname, c.app.config.options.mount_point)
|
417 |
app = '%s/%s' % (c.project.shortname, self.app_config.options.mount_point)
|
418 |
count = len(tickets)
|
418 |
count = len(tickets)
|
419 |
text = 'Updated {} ticket{} in {}'.format(count, 's' if count != 1 else '', app)
|
419 |
text = 'Updated {} ticket{} in {}'.format(count, 's' if count != 1 else '', app)
|
420 |
Notification.post_user(c.user, None, 'flash', text=text)
|
420 |
Notification.post_user(c.user, None, 'flash', text=text)
|
421 |
|
421 |
|
422 |
def filtered_by_subscription(self, tickets, project_id=None, app_config_id=None):
|
422 |
def filtered_by_subscription(self, tickets, project_id=None, app_config_id=None):
|
423 |
p_id = project_id if project_id else c.project._id
|
423 |
p_id = project_id if project_id else c.project._id
|
424 |
ac_id = app_config_id if app_config_id else c.app.config._id
|
424 |
ac_id = app_config_id if app_config_id else self.app_config_id
|
425 |
ticket_ids = tickets.keys()
|
425 |
ticket_ids = tickets.keys()
|
426 |
tickets_index_id = {ticket.index_id(): t_id for t_id, ticket in tickets.iteritems()}
|
426 |
tickets_index_id = {ticket.index_id(): t_id for t_id, ticket in tickets.iteritems()}
|
427 |
subscriptions = Mailbox.query.find({
|
427 |
subscriptions = Mailbox.query.find({
|
428 |
'project_id': p_id,
|
428 |
'project_id': p_id,
|
429 |
'app_config_id': ac_id,
|
429 |
'app_config_id': ac_id,
|
|
... |
|
... |
676 |
def open_or_closed(self):
|
676 |
def open_or_closed(self):
|
677 |
return 'closed' if self.status in c.app.globals.set_of_closed_status_names else 'open'
|
677 |
return 'closed' if self.status in c.app.globals.set_of_closed_status_names else 'open'
|
678 |
|
678 |
|
679 |
@property
|
679 |
@property
|
680 |
def monitoring_email(self):
|
680 |
def monitoring_email(self):
|
681 |
return c.app.config.options.get('TicketMonitoringEmail')
|
681 |
return self.app_config.options.get('TicketMonitoringEmail')
|
682 |
|
682 |
|
683 |
@property
|
683 |
@property
|
684 |
def notify_post(self):
|
684 |
def notify_post(self):
|
685 |
monitoring_type = c.app.config.options.get('TicketMonitoringType')
|
685 |
monitoring_type = self.app_config.options.get('TicketMonitoringType')
|
686 |
return monitoring_type == 'AllTicketChanges' or (
|
686 |
return monitoring_type == 'AllTicketChanges' or (
|
687 |
monitoring_type == 'AllPublicTicketChanges' and
|
687 |
monitoring_type == 'AllPublicTicketChanges' and
|
688 |
not self.private)
|
688 |
not self.private)
|
689 |
|
689 |
|
690 |
def get_custom_user(self, custom_user_field_name):
|
690 |
def get_custom_user(self, custom_user_field_name):
|