trac/notification/model.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2023 Edgewall Software
# Copyright (C) 2010 Robert Corsaro
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://trac.edgewall.org/log/.
from trac.util.datefmt import datetime_now, utc, to_utimestamp
__all__ = ['Subscription', 'Watch']
class Subscription(object):
__slots__ = ('env', 'values')
fields = ('id', 'sid', 'authenticated', 'distributor', 'format',
'priority', 'adverb', 'class')
def __init__(self, env):
self.env = env
self.values = {}
def __repr__(self):
values = ' '.join('%s=%r' % (name, self.values.get(name))
for name in self.fields)
return '<%s %s>' % (self.__class__.__name__, values)
def __getitem__(self, name):
if name not in self.fields:
raise KeyError(name)
return self.values.get(name)
def __setitem__(self, name, value):
if name not in self.fields:
raise KeyError(name)
self.values[name] = value
def _from_database(self, id, sid, authenticated, distributor, format,
priority, adverb, class_):
self['id'] = id
self['sid'] = sid
self['authenticated'] = int(authenticated)
self['distributor'] = distributor
self['format'] = format or None
self['priority'] = int(priority)
self['adverb'] = adverb
self['class'] = class_
@classmethod
def add(cls, env, subscription):
"""id and priority overwritten."""
with env.db_transaction as db:
priority = len(cls.find_by_sid_and_distributor(
env, subscription['sid'], subscription['authenticated'],
subscription['distributor'])) + 1
now = to_utimestamp(datetime_now(utc))
cursor = db.cursor()
cursor.execute("""
INSERT INTO
notify_subscription (time, changetime, sid, authenticated,
distributor, format, priority, adverb,
class)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(now, now, subscription['sid'], int(subscription['authenticated']),
subscription['distributor'], subscription['format'] or None,
int(priority), subscription['adverb'],
subscription['class']))
return db.get_last_id(cursor, 'notify_subscription')
@classmethod
def delete(cls, env, rule_id, sid=None, authenticated=None):
with env.db_transaction as db:
kwargs = {'id': rule_id}
if sid is not None or authenticated is not None:
kwargs['sid'] = sid
kwargs['authenticated'] = 1 if authenticated else 0
for sub in cls._find(env, **kwargs):
break
else:
return
db("DELETE FROM notify_subscription WHERE id=%s", (sub['id'],))
subs = cls.find_by_sid_and_distributor(
env, sub['sid'], sub['authenticated'], sub['distributor'])
now = to_utimestamp(datetime_now(utc))
values = [(new_priority, now, sub['id'])
for new_priority, sub in enumerate(subs, 1)
if new_priority != sub['priority']]
db.executemany("""
UPDATE notify_subscription
SET priority=%s, changetime=%s WHERE id=%s
""", values)
@classmethod
def move(cls, env, rule_id, priority, sid=None, authenticated=None):
with env.db_transaction as db:
kwargs = {'id': rule_id}
if sid is not None or authenticated is not None:
kwargs['sid'] = sid
kwargs['authenticated'] = 1 if authenticated else 0
for sub in cls._find(env, **kwargs):
break
else:
return
subs = cls.find_by_sid_and_distributor(
env, sub['sid'], sub['authenticated'], sub['distributor'])
if not (1 <= priority <= len(subs)):
return
for idx, sub in enumerate(subs):
if sub['id'] == rule_id:
break
else:
return
subs.insert(priority - 1, subs.pop(idx))
now = to_utimestamp(datetime_now(utc))
values = [(new_priority, now, sub['id'])
for new_priority, sub in enumerate(subs, 1)
if new_priority != sub['priority']]
db.executemany("""
UPDATE notify_subscription
SET priority=%s, changetime=%s WHERE id=%s
""", values)
@classmethod
def replace_all(cls, env, sid, authenticated, subscriptions):
authenticated = int(authenticated)
with env.db_transaction as db:
ids_map = {}
for id_, distributor, class_ in db("""\
SELECT id, distributor, class FROM notify_subscription
WHERE sid=%s AND authenticated=%s""",
(sid, authenticated)):
ids_map.setdefault((distributor, class_), []).append(id_)
for ids in ids_map.values():
ids.sort(reverse=True)
priorities = {}
now = to_utimestamp(datetime_now(utc))
for sub in subscriptions:
distributor = sub['distributor']
priorities.setdefault(distributor, 0)
priorities[distributor] += 1
prio = priorities[distributor]
key = (distributor, sub['class'])
if ids_map.get(key):
id_ = ids_map[key].pop()
db("""\
UPDATE notify_subscription
SET changetime=%s,distributor=%s,format=%s,priority=%s,
adverb=%s,class=%s
WHERE id=%s""",
(now, sub['distributor'], sub['format'] or None, prio,
sub['adverb'], sub['class'], id_))
else:
db("""\
INSERT INTO notify_subscription (
time,changetime,sid,authenticated,distributor,
format,priority,adverb,class)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
(now, now, sid, authenticated, sub['distributor'],
sub['format'] or None, prio, sub['adverb'],
sub['class']))
delete_ids = []
for ids in ids_map.values():
delete_ids.extend(ids)
if delete_ids:
db("DELETE FROM notify_subscription WHERE id IN (%s)" %
','.join(('%s',) * len(delete_ids)), delete_ids)
@classmethod
def update_format_by_distributor_and_sid(cls, env, distributor, sid,
authenticated, format):
with env.db_transaction as db:
db("""
UPDATE notify_subscription
SET format=%s
WHERE distributor=%s
AND sid=%s
AND authenticated=%s
""", (format or None, distributor, sid, int(authenticated)))
@classmethod
def _find(cls, env, order=None, **kwargs):
with env.db_query as db:
conditions = []
args = []
for name, value in sorted(kwargs.items()):
if name.endswith('_'):
name = name[:-1]
if name == 'authenticated':
value = int(value)
conditions.append(db.quote(name) + '=%s')
args.append(value)
query = 'SELECT id, sid, authenticated, distributor, format, ' \
'priority, adverb, class FROM notify_subscription'
if conditions:
query += ' WHERE ' + ' AND '.join(conditions)
if order:
if not isinstance(order, (tuple, list)):
order = (order,)
query += ' ORDER BY ' + \
', '.join(db.quote(name) for name in order)
cursor = db.cursor()
cursor.execute(query, args)
for row in cursor:
sub = Subscription(env)
sub._from_database(*row)
yield sub
@classmethod
def find_by_sid_and_distributor(cls, env, sid, authenticated, distributor):
return list(cls._find(env, sid=sid, authenticated=authenticated,
distributor=distributor, order='priority'))
@classmethod
def find_by_sids_and_class(cls, env, uids, class_):
"""uids should be a collection to tuples (sid, auth)"""
subs = []
for sid, authenticated in uids:
subs.extend(cls._find(env, class_=class_, sid=sid,
authenticated=authenticated,
order='priority'))
return subs
@classmethod
def find_by_class(cls, env, class_):
return list(cls._find(env, class_=class_))
def subscription_tuple(self):
return (
self.values['class'],
self.values['distributor'],
self.values['sid'],
self.values['authenticated'],
None,
self.values['format'] or None,
int(self.values['priority']),
self.values['adverb']
)
def _update_priority(self):
with self.env.db_transaction as db:
cursor = db.cursor()
now = to_utimestamp(datetime_now(utc))
cursor.execute("""
UPDATE notify_subscription
SET changetime=%s, priority=%s
WHERE id=%s
""", (now, int(self.values['priority']), self.values['id']))
class Watch(object):
__slots__ = ('env', 'values')
fields = ('id', 'sid', 'authenticated', 'class', 'realm', 'target')
def __init__(self, env):
self.env = env
self.values = {}
def __getitem__(self, name):
if name not in self.fields:
raise KeyError(name)
return self.values.get(name)
def __setitem__(self, name, value):
if name not in self.fields:
raise KeyError(name)
self.values[name] = value
def _from_database(self, id, sid, authenticated, class_, realm, target):
self['id'] = id
self['sid'] = sid
self['authenticated'] = int(authenticated)
self['class'] = class_
self['realm'] = realm
self['target'] = target
@classmethod
def add(cls, env, sid, authenticated, class_, realm, targets):
with env.db_transaction as db:
for target in targets:
db("""
INSERT INTO notify_watch (sid, authenticated, class,
realm, target)
VALUES (%s, %s, %s, %s, %s)
""", (sid, int(authenticated), class_, realm, target))
@classmethod
def delete(cls, env, watch_id):
with env.db_transaction as db:
db("DELETE FROM notify_watch WHERE id = %s", (watch_id,))
@classmethod
def delete_by_sid_and_class(cls, env, sid, authenticated, class_):
with env.db_transaction as db:
db("""
DELETE FROM notify_watch
WHERE sid = %s AND authenticated = %s AND class = %s
""", (sid, int(authenticated), class_))
@classmethod
def delete_by_class_realm_and_target(cls, env, class_, realm, target):
with env.db_transaction as db:
db("""
DELETE FROM notify_watch
WHERE class = %s AND realm = %s AND target = %s
""", (realm, class_, target))
@classmethod
def _find(cls, env, order=None, **kwargs):
with env.db_query as db:
conditions = []
args = []
for name, value in sorted(kwargs.items()):
if name.endswith('_'):
name = name[:-1]
if name == 'authenticated':
value = int(value)
conditions.append(db.quote(name) + '=%s')
if name == 'authenticated':
value = int(value)
args.append(value)
query = 'SELECT id, sid, authenticated, class, realm, target ' \
'FROM notify_watch'
if conditions:
query += ' WHERE ' + ' AND '.join(conditions)
if order:
if not isinstance(order, (tuple, list)):
order = (order,)
query += ' ORDER BY ' + \
', '.join(db.quote(name) for name in order)
cursor = db.cursor()
cursor.execute(query, args)
for row in cursor:
watch = Watch(env)
watch._from_database(*row)
yield watch
@classmethod
def find_by_sid_and_class(cls, env, sid, authenticated, class_):
return list(cls._find(env, sid=sid, authenticated=authenticated,
class_=class_, order='target'))
@classmethod
def find_by_sid_class_realm_and_target(cls, env, sid, authenticated,
class_, realm, target):
return list(cls._find(env, sid=sid, authenticated=authenticated,
class_=class_, realm=realm, order='target'))
@classmethod
def find_by_class_realm_and_target(cls, env, class_, realm, target):
return list(cls._find(env, class_=class_, realm=realm, target=target))
@classmethod
def find_by_class_and_realm(cls, env, class_, realm):
return list(cls._find(env, class_=class_, realm=realm))