From 5495accf05d77e1c4ff2855f5e42c2e56f51e45d Mon Sep 17 00:00:00 2001
From: Barry Warsaw
Date: Sat, 8 Dec 2007 11:51:36 -0500
Subject: Reorganize the database subpackage, primarily by removing the 'model'
subdirectory and updating all relevant imports. Move of the circular import
problems have been eliminated in the process.
---
Mailman/data/PythonPowered.png | Bin 945 -> 0 bytes
Mailman/data/__init__.py | 0
Mailman/data/coverage.py | 952 ----------------------------------
Mailman/data/gnu-head-tiny.jpg | Bin 3049 -> 0 bytes
Mailman/data/mailman-large.jpg | Bin 6150 -> 0 bytes
Mailman/data/mailman.cfg.in | 45 --
Mailman/data/mailman.in | 54 --
Mailman/data/mailman.jpg | Bin 2022 -> 0 bytes
Mailman/data/mm-icon.png | Bin 281 -> 0 bytes
Mailman/data/paths.py.in | 90 ----
Mailman/database/__init__.py | 102 ++--
Mailman/database/address.py | 86 +++
Mailman/database/language.py | 30 ++
Mailman/database/listmanager.py | 7 +-
Mailman/database/mailinglist.py | 257 +++++++++
Mailman/database/mailman.sql | 206 ++++++++
Mailman/database/member.py | 95 ++++
Mailman/database/message.py | 42 ++
Mailman/database/messagestore.py | 10 +-
Mailman/database/model.py | 55 ++
Mailman/database/model/__init__.py | 108 ----
Mailman/database/model/address.py | 86 ---
Mailman/database/model/language.py | 30 --
Mailman/database/model/mailinglist.py | 257 ---------
Mailman/database/model/mailman.sql | 206 --------
Mailman/database/model/member.py | 95 ----
Mailman/database/model/message.py | 42 --
Mailman/database/model/pending.py | 158 ------
Mailman/database/model/preferences.py | 40 --
Mailman/database/model/requests.py | 134 -----
Mailman/database/model/roster.py | 209 --------
Mailman/database/model/user.py | 74 ---
Mailman/database/model/version.py | 30 --
Mailman/database/pending.py | 158 ++++++
Mailman/database/preferences.py | 40 ++
Mailman/database/requests.py | 134 +++++
Mailman/database/roster.py | 210 ++++++++
Mailman/database/user.py | 74 +++
Mailman/database/usermanager.py | 15 +-
Mailman/database/version.py | 30 ++
data/PythonPowered.png | Bin 0 -> 945 bytes
data/__init__.py | 0
data/coverage.py | 952 ++++++++++++++++++++++++++++++++++
data/gnu-head-tiny.jpg | Bin 0 -> 3049 bytes
data/mailman-large.jpg | Bin 0 -> 6150 bytes
data/mailman.cfg.in | 45 ++
data/mailman.in | 54 ++
data/mailman.jpg | Bin 0 -> 2022 bytes
data/mm-icon.png | Bin 0 -> 281 bytes
data/paths.py.in | 90 ++++
50 files changed, 2632 insertions(+), 2670 deletions(-)
delete mode 100644 Mailman/data/PythonPowered.png
delete mode 100755 Mailman/data/__init__.py
delete mode 100644 Mailman/data/coverage.py
delete mode 100644 Mailman/data/gnu-head-tiny.jpg
delete mode 100644 Mailman/data/mailman-large.jpg
delete mode 100644 Mailman/data/mailman.cfg.in
delete mode 100644 Mailman/data/mailman.in
delete mode 100644 Mailman/data/mailman.jpg
delete mode 100644 Mailman/data/mm-icon.png
delete mode 100644 Mailman/data/paths.py.in
create mode 100644 Mailman/database/address.py
create mode 100644 Mailman/database/language.py
create mode 100644 Mailman/database/mailinglist.py
create mode 100644 Mailman/database/mailman.sql
create mode 100644 Mailman/database/member.py
create mode 100644 Mailman/database/message.py
create mode 100644 Mailman/database/model.py
delete mode 100644 Mailman/database/model/__init__.py
delete mode 100644 Mailman/database/model/address.py
delete mode 100644 Mailman/database/model/language.py
delete mode 100644 Mailman/database/model/mailinglist.py
delete mode 100644 Mailman/database/model/mailman.sql
delete mode 100644 Mailman/database/model/member.py
delete mode 100644 Mailman/database/model/message.py
delete mode 100644 Mailman/database/model/pending.py
delete mode 100644 Mailman/database/model/preferences.py
delete mode 100644 Mailman/database/model/requests.py
delete mode 100644 Mailman/database/model/roster.py
delete mode 100644 Mailman/database/model/user.py
delete mode 100644 Mailman/database/model/version.py
create mode 100644 Mailman/database/pending.py
create mode 100644 Mailman/database/preferences.py
create mode 100644 Mailman/database/requests.py
create mode 100644 Mailman/database/roster.py
create mode 100644 Mailman/database/user.py
create mode 100644 Mailman/database/version.py
create mode 100644 data/PythonPowered.png
create mode 100755 data/__init__.py
create mode 100644 data/coverage.py
create mode 100644 data/gnu-head-tiny.jpg
create mode 100644 data/mailman-large.jpg
create mode 100644 data/mailman.cfg.in
create mode 100644 data/mailman.in
create mode 100644 data/mailman.jpg
create mode 100644 data/mm-icon.png
create mode 100644 data/paths.py.in
diff --git a/Mailman/data/PythonPowered.png b/Mailman/data/PythonPowered.png
deleted file mode 100644
index 2e9d99c2d..000000000
Binary files a/Mailman/data/PythonPowered.png and /dev/null differ
diff --git a/Mailman/data/__init__.py b/Mailman/data/__init__.py
deleted file mode 100755
index e69de29bb..000000000
diff --git a/Mailman/data/coverage.py b/Mailman/data/coverage.py
deleted file mode 100644
index 66e55e0c4..000000000
--- a/Mailman/data/coverage.py
+++ /dev/null
@@ -1,952 +0,0 @@
-#!/usr/bin/python
-#
-# Perforce Defect Tracking Integration Project
-#
-#
-# COVERAGE.PY -- COVERAGE TESTING
-#
-# Gareth Rees, Ravenbrook Limited, 2001-12-04
-# Ned Batchelder, 2004-12-12
-# http://nedbatchelder.com/code/modules/coverage.html
-#
-#
-# 1. INTRODUCTION
-#
-# This module provides coverage testing for Python code.
-#
-# The intended readership is all Python developers.
-#
-# This document is not confidential.
-#
-# See [GDR 2001-12-04a] for the command-line interface, programmatic
-# interface and limitations. See [GDR 2001-12-04b] for requirements and
-# design.
-
-r"""Usage:
-
-coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...]
- Execute module, passing the given command-line arguments, collecting
- coverage data. With the -p option, write to a temporary file containing
- the machine name and process ID.
-
-coverage.py -e
- Erase collected coverage data.
-
-coverage.py -c
- Collect data from multiple coverage files (as created by -p option above)
- and store it into a single file representing the union of the coverage.
-
-coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
- Report on the statement coverage for the given files. With the -m
- option, show line numbers of the statements that weren't executed.
-
-coverage.py -a [-d dir] [-o dir1,dir2,...] FILE1 FILE2 ...
- Make annotated copies of the given files, marking statements that
- are executed with > and statements that are missed with !. With
- the -d option, make the copies in that directory. Without the -d
- option, make each copy in the same directory as the original.
-
--o dir,dir2,...
- Omit reporting or annotating files when their filename path starts with
- a directory listed in the omit list.
- e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits
-
-Coverage data is saved in the file .coverage by default. Set the
-COVERAGE_FILE environment variable to save it somewhere else."""
-
-__version__ = "2.6.20060823" # see detailed history at the end of this file.
-
-import compiler
-import compiler.visitor
-import os
-import re
-import string
-import sys
-import threading
-import types
-from socket import gethostname
-
-# 2. IMPLEMENTATION
-#
-# This uses the "singleton" pattern.
-#
-# The word "morf" means a module object (from which the source file can
-# be deduced by suitable manipulation of the __file__ attribute) or a
-# filename.
-#
-# When we generate a coverage report we have to canonicalize every
-# filename in the coverage dictionary just in case it refers to the
-# module we are reporting on. It seems a shame to throw away this
-# information so the data in the coverage dictionary is transferred to
-# the 'cexecuted' dictionary under the canonical filenames.
-#
-# The coverage dictionary is called "c" and the trace function "t". The
-# reason for these short names is that Python looks up variables by name
-# at runtime and so execution time depends on the length of variables!
-# In the bottleneck of this application it's appropriate to abbreviate
-# names to increase speed.
-
-class StatementFindingAstVisitor(compiler.visitor.ASTVisitor):
- def __init__(self, statements, excluded, suite_spots):
- compiler.visitor.ASTVisitor.__init__(self)
- self.statements = statements
- self.excluded = excluded
- self.suite_spots = suite_spots
- self.excluding_suite = 0
-
- def doRecursive(self, node):
- self.recordNodeLine(node)
- for n in node.getChildNodes():
- self.dispatch(n)
-
- visitStmt = visitModule = doRecursive
-
- def doCode(self, node):
- if hasattr(node, 'decorators') and node.decorators:
- self.dispatch(node.decorators)
- self.recordAndDispatch(node.code)
- else:
- self.doSuite(node, node.code)
-
- visitFunction = visitClass = doCode
-
- def getFirstLine(self, node):
- # Find the first line in the tree node.
- lineno = node.lineno
- for n in node.getChildNodes():
- f = self.getFirstLine(n)
- if lineno and f:
- lineno = min(lineno, f)
- else:
- lineno = lineno or f
- return lineno
-
- def getLastLine(self, node):
- # Find the first line in the tree node.
- lineno = node.lineno
- for n in node.getChildNodes():
- lineno = max(lineno, self.getLastLine(n))
- return lineno
-
- def doStatement(self, node):
- self.recordLine(self.getFirstLine(node))
-
- visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \
- visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
- doStatement
-
- def recordNodeLine(self, node):
- return self.recordLine(node.lineno)
-
- def recordLine(self, lineno):
- # Returns a bool, whether the line is included or excluded.
- if lineno:
- # Multi-line tests introducing suites have to get charged to their
- # keyword.
- if lineno in self.suite_spots:
- lineno = self.suite_spots[lineno][0]
- # If we're inside an exluded suite, record that this line was
- # excluded.
- if self.excluding_suite:
- self.excluded[lineno] = 1
- return 0
- # If this line is excluded, or suite_spots maps this line to
- # another line that is exlcuded, then we're excluded.
- elif self.excluded.has_key(lineno) or \
- self.suite_spots.has_key(lineno) and \
- self.excluded.has_key(self.suite_spots[lineno][1]):
- return 0
- # Otherwise, this is an executable line.
- else:
- self.statements[lineno] = 1
- return 1
- return 0
-
- default = recordNodeLine
-
- def recordAndDispatch(self, node):
- self.recordNodeLine(node)
- self.dispatch(node)
-
- def doSuite(self, intro, body, exclude=0):
- exsuite = self.excluding_suite
- if exclude or (intro and not self.recordNodeLine(intro)):
- self.excluding_suite = 1
- self.recordAndDispatch(body)
- self.excluding_suite = exsuite
-
- def doPlainWordSuite(self, prevsuite, suite):
- # Finding the exclude lines for else's is tricky, because they aren't
- # present in the compiler parse tree. Look at the previous suite,
- # and find its last line. If any line between there and the else's
- # first line are excluded, then we exclude the else.
- lastprev = self.getLastLine(prevsuite)
- firstelse = self.getFirstLine(suite)
- for l in range(lastprev+1, firstelse):
- if self.suite_spots.has_key(l):
- self.doSuite(None, suite, exclude=self.excluded.has_key(l))
- break
- else:
- self.doSuite(None, suite)
-
- def doElse(self, prevsuite, node):
- if node.else_:
- self.doPlainWordSuite(prevsuite, node.else_)
-
- def visitFor(self, node):
- self.doSuite(node, node.body)
- self.doElse(node.body, node)
-
- def visitIf(self, node):
- # The first test has to be handled separately from the rest.
- # The first test is credited to the line with the "if", but the others
- # are credited to the line with the test for the elif.
- self.doSuite(node, node.tests[0][1])
- for t, n in node.tests[1:]:
- self.doSuite(t, n)
- self.doElse(node.tests[-1][1], node)
-
- def visitWhile(self, node):
- self.doSuite(node, node.body)
- self.doElse(node.body, node)
-
- def visitTryExcept(self, node):
- self.doSuite(node, node.body)
- for i in range(len(node.handlers)):
- a, b, h = node.handlers[i]
- if not a:
- # It's a plain "except:". Find the previous suite.
- if i > 0:
- prev = node.handlers[i-1][2]
- else:
- prev = node.body
- self.doPlainWordSuite(prev, h)
- else:
- self.doSuite(a, h)
- self.doElse(node.handlers[-1][2], node)
-
- def visitTryFinally(self, node):
- self.doSuite(node, node.body)
- self.doPlainWordSuite(node.body, node.final)
-
- def visitGlobal(self, node):
- # "global" statements don't execute like others (they don't call the
- # trace function), so don't record their line numbers.
- pass
-
-the_coverage = None
-
-class CoverageException(Exception): pass
-
-class coverage:
- # Name of the cache file (unless environment variable is set).
- cache_default = ".coverage"
-
- # Environment variable naming the cache file.
- cache_env = "COVERAGE_FILE"
-
- # A dictionary with an entry for (Python source file name, line number
- # in that file) if that line has been executed.
- c = {}
-
- # A map from canonical Python source file name to a dictionary in
- # which there's an entry for each line number that has been
- # executed.
- cexecuted = {}
-
- # Cache of results of calling the analysis2() method, so that you can
- # specify both -r and -a without doing double work.
- analysis_cache = {}
-
- # Cache of results of calling the canonical_filename() method, to
- # avoid duplicating work.
- canonical_filename_cache = {}
-
- def __init__(self):
- global the_coverage
- if the_coverage:
- raise CoverageException, "Only one coverage object allowed."
- self.usecache = 1
- self.cache = None
- self.exclude_re = ''
- self.nesting = 0
- self.cstack = []
- self.xstack = []
- self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep)
-
- # t(f, x, y). This method is passed to sys.settrace as a trace function.
- # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and
- # the arguments and return value of the trace function.
- # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code
- # objects.
-
- def t(self, f, w, a): #pragma: no cover
- if w == 'line':
- self.c[(f.f_code.co_filename, f.f_lineno)] = 1
- for c in self.cstack:
- c[(f.f_code.co_filename, f.f_lineno)] = 1
- return self.t
-
- def help(self, error=None):
- if error:
- print error
- print
- print __doc__
- sys.exit(1)
-
- def command_line(self, argv, help=None):
- import getopt
- help = help or self.help
- settings = {}
- optmap = {
- '-a': 'annotate',
- '-c': 'collect',
- '-d:': 'directory=',
- '-e': 'erase',
- '-h': 'help',
- '-i': 'ignore-errors',
- '-m': 'show-missing',
- '-p': 'parallel-mode',
- '-r': 'report',
- '-x': 'execute',
- '-o:': 'omit=',
- }
- short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
- long_opts = optmap.values()
- options, args = getopt.getopt(argv, short_opts, long_opts)
- for o, a in options:
- if optmap.has_key(o):
- settings[optmap[o]] = 1
- elif optmap.has_key(o + ':'):
- settings[optmap[o + ':']] = a
- elif o[2:] in long_opts:
- settings[o[2:]] = 1
- elif o[2:] + '=' in long_opts:
- settings[o[2:]+'='] = a
- else: #pragma: no cover
- pass # Can't get here, because getopt won't return anything unknown.
-
- if settings.get('help'):
- help()
-
- for i in ['erase', 'execute']:
- for j in ['annotate', 'report', 'collect']:
- if settings.get(i) and settings.get(j):
- help("You can't specify the '%s' and '%s' "
- "options at the same time." % (i, j))
-
- args_needed = (settings.get('execute')
- or settings.get('annotate')
- or settings.get('report'))
- action = (settings.get('erase')
- or settings.get('collect')
- or args_needed)
- if not action:
- help("You must specify at least one of -e, -x, -c, -r, or -a.")
- if not args_needed and args:
- help("Unexpected arguments: %s" % " ".join(args))
-
- self.get_ready(settings.get('parallel-mode'))
- self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
-
- if settings.get('erase'):
- self.erase()
- if settings.get('execute'):
- if not args:
- help("Nothing to do.")
- sys.argv = args
- self.start()
- import __main__
- sys.path[0] = os.path.dirname(sys.argv[0])
- execfile(sys.argv[0], __main__.__dict__)
- if settings.get('collect'):
- self.collect()
- if not args:
- args = self.cexecuted.keys()
-
- ignore_errors = settings.get('ignore-errors')
- show_missing = settings.get('show-missing')
- directory = settings.get('directory=')
-
- omit = settings.get('omit=')
- if omit is not None:
- omit = omit.split(',')
- else:
- omit = []
-
- if settings.get('report'):
- self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
- if settings.get('annotate'):
- self.annotate(args, directory, ignore_errors, omit_prefixes=omit)
-
- def use_cache(self, usecache, cache_file=None):
- self.usecache = usecache
- if cache_file and not self.cache:
- self.cache_default = cache_file
-
- def get_ready(self, parallel_mode=False):
- if self.usecache and not self.cache:
- self.cache = os.environ.get(self.cache_env, self.cache_default)
- if parallel_mode:
- self.cache += "." + gethostname() + "." + str(os.getpid())
- self.restore()
- self.analysis_cache = {}
-
- def start(self, parallel_mode=False):
- self.get_ready(parallel_mode)
- if self.nesting == 0: #pragma: no cover
- sys.settrace(self.t)
- if hasattr(threading, 'settrace'):
- threading.settrace(self.t)
- self.nesting += 1
-
- def stop(self):
- self.nesting -= 1
- if self.nesting == 0: #pragma: no cover
- sys.settrace(None)
- if hasattr(threading, 'settrace'):
- threading.settrace(None)
-
- def erase(self):
- self.c = {}
- self.analysis_cache = {}
- self.cexecuted = {}
- if self.cache and os.path.exists(self.cache):
- os.remove(self.cache)
- self.exclude_re = ""
-
- def exclude(self, re):
- if self.exclude_re:
- self.exclude_re += "|"
- self.exclude_re += "(" + re + ")"
-
- def begin_recursive(self):
- self.cstack.append(self.c)
- self.xstack.append(self.exclude_re)
-
- def end_recursive(self):
- self.c = self.cstack.pop()
- self.exclude_re = self.xstack.pop()
-
- # save(). Save coverage data to the coverage cache.
-
- def save(self):
- if self.usecache and self.cache:
- self.canonicalize_filenames()
- cache = open(self.cache, 'wb')
- import marshal
- marshal.dump(self.cexecuted, cache)
- cache.close()
-
- # restore(). Restore coverage data from the coverage cache (if it exists).
-
- def restore(self):
- self.c = {}
- self.cexecuted = {}
- assert self.usecache
- if os.path.exists(self.cache):
- self.cexecuted = self.restore_file(self.cache)
-
- def restore_file(self, file_name):
- try:
- cache = open(file_name, 'rb')
- import marshal
- cexecuted = marshal.load(cache)
- cache.close()
- if isinstance(cexecuted, types.DictType):
- return cexecuted
- else:
- return {}
- except:
- return {}
-
- # collect(). Collect data in multiple files produced by parallel mode
-
- def collect(self):
- cache_dir, local = os.path.split(self.cache)
- for file in os.listdir(cache_dir):
- if not file.startswith(local):
- continue
-
- full_path = os.path.join(cache_dir, file)
- cexecuted = self.restore_file(full_path)
- self.merge_data(cexecuted)
-
- def merge_data(self, new_data):
- for file_name, file_data in new_data.items():
- if self.cexecuted.has_key(file_name):
- self.merge_file_data(self.cexecuted[file_name], file_data)
- else:
- self.cexecuted[file_name] = file_data
-
- def merge_file_data(self, cache_data, new_data):
- for line_number in new_data.keys():
- if not cache_data.has_key(line_number):
- cache_data[line_number] = new_data[line_number]
-
- # canonical_filename(filename). Return a canonical filename for the
- # file (that is, an absolute path with no redundant components and
- # normalized case). See [GDR 2001-12-04b, 3.3].
-
- def canonical_filename(self, filename):
- if not self.canonical_filename_cache.has_key(filename):
- f = filename
- if os.path.isabs(f) and not os.path.exists(f):
- f = os.path.basename(f)
- if not os.path.isabs(f):
- for path in [os.curdir] + sys.path:
- g = os.path.join(path, f)
- if os.path.exists(g):
- f = g
- break
- cf = os.path.normcase(os.path.abspath(f))
- self.canonical_filename_cache[filename] = cf
- return self.canonical_filename_cache[filename]
-
- # canonicalize_filenames(). Copy results from "c" to "cexecuted",
- # canonicalizing filenames on the way. Clear the "c" map.
-
- def canonicalize_filenames(self):
- for filename, lineno in self.c.keys():
- f = self.canonical_filename(filename)
- if not self.cexecuted.has_key(f):
- self.cexecuted[f] = {}
- self.cexecuted[f][lineno] = 1
- self.c = {}
-
- # morf_filename(morf). Return the filename for a module or file.
-
- def morf_filename(self, morf):
- if isinstance(morf, types.ModuleType):
- if not hasattr(morf, '__file__'):
- raise CoverageException, "Module has no __file__ attribute."
- file = morf.__file__
- else:
- file = morf
- return self.canonical_filename(file)
-
- # analyze_morf(morf). Analyze the module or filename passed as
- # the argument. If the source code can't be found, raise an error.
- # Otherwise, return a tuple of (1) the canonical filename of the
- # source code for the module, (2) a list of lines of statements
- # in the source code, and (3) a list of lines of excluded statements.
-
- def analyze_morf(self, morf):
- if self.analysis_cache.has_key(morf):
- return self.analysis_cache[morf]
- filename = self.morf_filename(morf)
- ext = os.path.splitext(filename)[1]
- if ext == '.pyc':
- if not os.path.exists(filename[0:-1]):
- raise CoverageException, ("No source for compiled code '%s'."
- % filename)
- filename = filename[0:-1]
- elif ext != '.py':
- raise CoverageException, "File '%s' not Python source." % filename
- source = open(filename, 'r')
- lines, excluded_lines = self.find_executable_statements(
- source.read(), exclude=self.exclude_re
- )
- source.close()
- result = filename, lines, excluded_lines
- self.analysis_cache[morf] = result
- return result
-
- def get_suite_spots(self, tree, spots):
- import symbol, token
- for i in range(1, len(tree)):
- if type(tree[i]) == type(()):
- if tree[i][0] == symbol.suite:
- # Found a suite, look back for the colon and keyword.
- lineno_colon = lineno_word = None
- for j in range(i-1, 0, -1):
- if tree[j][0] == token.COLON:
- lineno_colon = tree[j][2]
- elif tree[j][0] == token.NAME:
- if tree[j][1] == 'elif':
- # Find the line number of the first non-terminal
- # after the keyword.
- t = tree[j+1]
- while t and token.ISNONTERMINAL(t[0]):
- t = t[1]
- if t:
- lineno_word = t[2]
- else:
- lineno_word = tree[j][2]
- break
- elif tree[j][0] == symbol.except_clause:
- # "except" clauses look like:
- # ('except_clause', ('NAME', 'except', lineno), ...)
- if tree[j][1][0] == token.NAME:
- lineno_word = tree[j][1][2]
- break
- if lineno_colon and lineno_word:
- # Found colon and keyword, mark all the lines
- # between the two with the two line numbers.
- for l in range(lineno_word, lineno_colon+1):
- spots[l] = (lineno_word, lineno_colon)
- self.get_suite_spots(tree[i], spots)
-
- def find_executable_statements(self, text, exclude=None):
- # Find lines which match an exclusion pattern.
- excluded = {}
- suite_spots = {}
- if exclude:
- reExclude = re.compile(exclude)
- lines = text.split('\n')
- for i in range(len(lines)):
- if reExclude.search(lines[i]):
- excluded[i+1] = 1
-
- import parser
- tree = parser.suite(text+'\n\n').totuple(1)
- self.get_suite_spots(tree, suite_spots)
-
- # Use the compiler module to parse the text and find the executable
- # statements. We add newlines to be impervious to final partial lines.
- statements = {}
- ast = compiler.parse(text+'\n\n')
- visitor = StatementFindingAstVisitor(statements, excluded, suite_spots)
- compiler.walk(ast, visitor, walker=visitor)
-
- lines = statements.keys()
- lines.sort()
- excluded_lines = excluded.keys()
- excluded_lines.sort()
- return lines, excluded_lines
-
- # format_lines(statements, lines). Format a list of line numbers
- # for printing by coalescing groups of lines as long as the lines
- # represent consecutive statements. This will coalesce even if
- # there are gaps between statements, so if statements =
- # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then
- # format_lines will return "1-2, 5-11, 13-14".
-
- def format_lines(self, statements, lines):
- pairs = []
- i = 0
- j = 0
- start = None
- pairs = []
- while i < len(statements) and j < len(lines):
- if statements[i] == lines[j]:
- if start == None:
- start = lines[j]
- end = lines[j]
- j = j + 1
- elif start:
- pairs.append((start, end))
- start = None
- i = i + 1
- if start:
- pairs.append((start, end))
- def stringify(pair):
- start, end = pair
- if start == end:
- return "%d" % start
- else:
- return "%d-%d" % (start, end)
- return string.join(map(stringify, pairs), ", ")
-
- # Backward compatibility with version 1.
- def analysis(self, morf):
- f, s, _, m, mf = self.analysis2(morf)
- return f, s, m, mf
-
- def analysis2(self, morf):
- filename, statements, excluded = self.analyze_morf(morf)
- self.canonicalize_filenames()
- if not self.cexecuted.has_key(filename):
- self.cexecuted[filename] = {}
- missing = []
- for line in statements:
- if not self.cexecuted[filename].has_key(line):
- missing.append(line)
- return (filename, statements, excluded, missing,
- self.format_lines(statements, missing))
-
- def relative_filename(self, filename):
- """ Convert filename to relative filename from self.relative_dir.
- """
- return filename.replace(self.relative_dir, "")
-
- def morf_name(self, morf):
- """ Return the name of morf as used in report.
- """
- if isinstance(morf, types.ModuleType):
- return morf.__name__
- else:
- return self.relative_filename(os.path.splitext(morf)[0])
-
- def filter_by_prefix(self, morfs, omit_prefixes):
- """ Return list of morfs where the morf name does not begin
- with any one of the omit_prefixes.
- """
- filtered_morfs = []
- for morf in morfs:
- for prefix in omit_prefixes:
- if self.morf_name(morf).startswith(prefix):
- break
- else:
- filtered_morfs.append(morf)
-
- return filtered_morfs
-
- def morf_name_compare(self, x, y):
- return cmp(self.morf_name(x), self.morf_name(y))
-
- def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]):
- if not isinstance(morfs, types.ListType):
- morfs = [morfs]
- morfs = self.filter_by_prefix(morfs, omit_prefixes)
- morfs.sort(self.morf_name_compare)
-
- max_name = max([5,] + map(len, map(self.morf_name, morfs)))
- fmt_name = "%%- %ds " % max_name
- fmt_err = fmt_name + "%s: %s"
- header = fmt_name % "Name" + " Stmts Exec Cover"
- fmt_coverage = fmt_name + "% 6d % 6d % 5d%%"
- if show_missing:
- header = header + " Missing"
- fmt_coverage = fmt_coverage + " %s"
- if not file:
- file = sys.stdout
- print >>file, header
- print >>file, "-" * len(header)
- total_statements = 0
- total_executed = 0
- for morf in morfs:
- name = self.morf_name(morf)
- try:
- _, statements, _, missing, readable = self.analysis2(morf)
- n = len(statements)
- m = n - len(missing)
- if n > 0:
- pc = 100.0 * m / n
- else:
- pc = 100.0
- args = (name, n, m, pc)
- if show_missing:
- args = args + (readable,)
- print >>file, fmt_coverage % args
- total_statements = total_statements + n
- total_executed = total_executed + m
- except KeyboardInterrupt: #pragma: no cover
- raise
- except:
- if not ignore_errors:
- type, msg = sys.exc_info()[0:2]
- print >>file, fmt_err % (name, type, msg)
- if len(morfs) > 1:
- print >>file, "-" * len(header)
- if total_statements > 0:
- pc = 100.0 * total_executed / total_statements
- else:
- pc = 100.0
- args = ("TOTAL", total_statements, total_executed, pc)
- if show_missing:
- args = args + ("",)
- print >>file, fmt_coverage % args
-
- # annotate(morfs, ignore_errors).
-
- blank_re = re.compile(r"\s*(#|$)")
- else_re = re.compile(r"\s*else\s*:\s*(#|$)")
-
- def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]):
- morfs = self.filter_by_prefix(morfs, omit_prefixes)
- for morf in morfs:
- try:
- filename, statements, excluded, missing, _ = self.analysis2(morf)
- self.annotate_file(filename, statements, excluded, missing, directory)
- except KeyboardInterrupt:
- raise
- except:
- if not ignore_errors:
- raise
-
- def annotate_file(self, filename, statements, excluded, missing, directory=None):
- source = open(filename, 'r')
- if directory:
- dest_file = os.path.join(directory,
- os.path.basename(filename)
- + ',cover')
- else:
- dest_file = filename + ',cover'
- dest = open(dest_file, 'w')
- lineno = 0
- i = 0
- j = 0
- covered = 1
- while 1:
- line = source.readline()
- if line == '':
- break
- lineno = lineno + 1
- while i < len(statements) and statements[i] < lineno:
- i = i + 1
- while j < len(missing) and missing[j] < lineno:
- j = j + 1
- if i < len(statements) and statements[i] == lineno:
- covered = j >= len(missing) or missing[j] > lineno
- if self.blank_re.match(line):
- dest.write(' ')
- elif self.else_re.match(line):
- # Special logic for lines containing only 'else:'.
- # See [GDR 2001-12-04b, 3.2].
- if i >= len(statements) and j >= len(missing):
- dest.write('! ')
- elif i >= len(statements) or j >= len(missing):
- dest.write('> ')
- elif statements[i] == missing[j]:
- dest.write('! ')
- else:
- dest.write('> ')
- elif lineno in excluded:
- dest.write('- ')
- elif covered:
- dest.write('> ')
- else:
- dest.write('! ')
- dest.write(line)
- source.close()
- dest.close()
-
-# Singleton object.
-the_coverage = coverage()
-
-# Module functions call methods in the singleton object.
-def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw)
-def start(*args, **kw): return the_coverage.start(*args, **kw)
-def stop(*args, **kw): return the_coverage.stop(*args, **kw)
-def erase(*args, **kw): return the_coverage.erase(*args, **kw)
-def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw)
-def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw)
-def exclude(*args, **kw): return the_coverage.exclude(*args, **kw)
-def analysis(*args, **kw): return the_coverage.analysis(*args, **kw)
-def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw)
-def report(*args, **kw): return the_coverage.report(*args, **kw)
-def annotate(*args, **kw): return the_coverage.annotate(*args, **kw)
-def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw)
-
-# Save coverage data when Python exits. (The atexit module wasn't
-# introduced until Python 2.0, so use sys.exitfunc when it's not
-# available.)
-try:
- import atexit
- atexit.register(the_coverage.save)
-except ImportError:
- sys.exitfunc = the_coverage.save
-
-# Command-line interface.
-if __name__ == '__main__':
- the_coverage.command_line(sys.argv[1:])
-
-
-# A. REFERENCES
-#
-# [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees;
-# Ravenbrook Limited; 2001-12-04;
-# .
-#
-# [GDR 2001-12-04b] "Statement coverage for Python: design and
-# analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04;
-# .
-#
-# [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)";
-# Guide van Rossum; 2001-07-20;
-# .
-#
-# [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum;
-# 2001-07-20; .
-#
-#
-# B. DOCUMENT HISTORY
-#
-# 2001-12-04 GDR Created.
-#
-# 2001-12-06 GDR Added command-line interface and source code
-# annotation.
-#
-# 2001-12-09 GDR Moved design and interface to separate documents.
-#
-# 2001-12-10 GDR Open cache file as binary on Windows. Allow
-# simultaneous -e and -x, or -a and -r.
-#
-# 2001-12-12 GDR Added command-line help. Cache analysis so that it
-# only needs to be done once when you specify -a and -r.
-#
-# 2001-12-13 GDR Improved speed while recording. Portable between
-# Python 1.5.2 and 2.1.1.
-#
-# 2002-01-03 GDR Module-level functions work correctly.
-#
-# 2002-01-07 GDR Update sys.path when running a file with the -x option,
-# so that it matches the value the program would get if it were run on
-# its own.
-#
-# 2004-12-12 NMB Significant code changes.
-# - Finding executable statements has been rewritten so that docstrings and
-# other quirks of Python execution aren't mistakenly identified as missing
-# lines.
-# - Lines can be excluded from consideration, even entire suites of lines.
-# - The filesystem cache of covered lines can be disabled programmatically.
-# - Modernized the code.
-#
-# 2004-12-14 NMB Minor tweaks. Return 'analysis' to its original behavior
-# and add 'analysis2'. Add a global for 'annotate', and factor it, adding
-# 'annotate_file'.
-#
-# 2004-12-31 NMB Allow for keyword arguments in the module global functions.
-# Thanks, Allen.
-#
-# 2005-12-02 NMB Call threading.settrace so that all threads are measured.
-# Thanks Martin Fuzzey. Add a file argument to report so that reports can be
-# captured to a different destination.
-#
-# 2005-12-03 NMB coverage.py can now measure itself.
-#
-# 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames,
-# and sorting and omitting files to report on.
-#
-# 2006-07-23 NMB Applied Joseph Tate's patch for function decorators.
-#
-# 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument
-# handling.
-#
-# 2006-08-22 NMB Applied Geoff Bache's parallel mode patch.
-#
-# 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line
-# logic for parallel mode and collect.
-
-# C. COPYRIGHT AND LICENCE
-#
-# Copyright 2001 Gareth Rees. All rights reserved.
-# Copyright 2004-2006 Ned Batchelder. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the
-# distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-# DAMAGE.
-#
-# $Id: coverage.py 47 2006-08-24 01:08:48Z Ned $
diff --git a/Mailman/data/gnu-head-tiny.jpg b/Mailman/data/gnu-head-tiny.jpg
deleted file mode 100644
index 441be50d6..000000000
Binary files a/Mailman/data/gnu-head-tiny.jpg and /dev/null differ
diff --git a/Mailman/data/mailman-large.jpg b/Mailman/data/mailman-large.jpg
deleted file mode 100644
index e184f3c6e..000000000
Binary files a/Mailman/data/mailman-large.jpg and /dev/null differ
diff --git a/Mailman/data/mailman.cfg.in b/Mailman/data/mailman.cfg.in
deleted file mode 100644
index 3e24acc46..000000000
--- a/Mailman/data/mailman.cfg.in
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- python -*-
-
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""This module contains your site-specific settings.
-
-Use this to override the default settings in `Mailman/Defaults.py`. You only
-need to include those settings that you want to change, and unlike the old
-`mm_cfg.py` file, you do /not/ need to `import Defaults`. Its variables will
-automatically be available in this module's namespace.
-
-You should consult `Defaults.py` though for a complete listing of
-configuration variables that you can change.
-
-Mailman's installation procedure will never overwrite `mailman.cfg`.
-"""
-
-# This is the top-level run-time data directory. All other runtime data by
-# default lives under this directory.
-VAR_DIR = '$var_dir'
-
-# User name and id that owns the Mailman process and files.
-MAILMAN_UID = '$user_id'
-MAILMAN_USER = '$user_name'
-
-# Group name and id that owns the Mailman process and files.
-MAILMAN_GID = '$group_id'
-MAILMAN_GROUP = '$group_name'
-
-LANGUAGES = '$languages'
diff --git a/Mailman/data/mailman.in b/Mailman/data/mailman.in
deleted file mode 100644
index 4fb0612f3..000000000
--- a/Mailman/data/mailman.in
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-#
-# mailman This shell script starts and stops GNU Mailman.
-#
-# Copyright (C) 2001-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-# Copy this file to /etc/init.d/ (or /etc/rc.d/init.d/ depending on
-# your system) and activate it as such:
-#
-# On Debian, type "update-rc.d mailman defaults"
-# On RedHat, and derivatives, install with "chkconfig --add mailman"
-#
-# chkconfig: 2345 98 12
-# description: Mailman is the GNU Mailing List Manager, a program that \
-# manages electronic mail discussion groups. For more \
-# on GNU Mailman see http://www.list.org
-# processname: mailmanctl
-# config: @prefix@/Mailman/mm_cfg.py
-# pidfile: @prefix@/data/master-qrunner.pid
-
-PYTHON=@PYTHON@
-MAILMANHOME=@prefix@
-MAILMANCTL=$MAILMANHOME/bin/mailmanctl
-
-case "$1" in
-'start')
- #rm -f $MAILMANHOME/locks/*
- $PYTHON $MAILMANCTL -s -q start
- ;;
-
-'stop')
- $PYTHON $MAILMANCTL -q stop
- ;;
-
-'restart')
- $PYTHON $MAILMANCTL -q restart
- ;;
-
-esac
-exit 0
diff --git a/Mailman/data/mailman.jpg b/Mailman/data/mailman.jpg
deleted file mode 100644
index 94a4c0112..000000000
Binary files a/Mailman/data/mailman.jpg and /dev/null differ
diff --git a/Mailman/data/mm-icon.png b/Mailman/data/mm-icon.png
deleted file mode 100644
index 42c3cf759..000000000
Binary files a/Mailman/data/mm-icon.png and /dev/null differ
diff --git a/Mailman/data/paths.py.in b/Mailman/data/paths.py.in
deleted file mode 100644
index a840e3f14..000000000
--- a/Mailman/data/paths.py.in
+++ /dev/null
@@ -1,90 +0,0 @@
-# -*- python -*-
-
-# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-# configure turns this file into paths.py which is installed in the bin
-# directory. By importing this module, sys.path gets `hacked' so that the
-# $prefix/Mailman and $prefix/pythonlib directories are inserted at the start
-# of that list. This file exports two attributes that other modules may use
-# to get the absolute path to the installed Mailman distribution.
-#
-# Note that we can't use site.addsitedir() because that ends up appending
-# directories to sys.path and we really need to add them to the front so that
-# they override anything in the system Python.
-
-import os
-import sys
-
-# Some scripts expect this attribute to be in this module
-prefix = '@prefix@'
-exec_prefix = '@exec_prefix@'
-
-# Work around a bogus autoconf 2.12 bug
-if exec_prefix == '${prefix}':
- exec_prefix = prefix
-
-pythonlib = os.path.join(prefix, 'pythonlib', 'lib', 'python')
-
-# Hack the path to include the parent directory of $prefix/Mailman
-sys.path.insert(0, prefix)
-
-# Much of this is ripped off from site.py
-paths = set()
-for dirname in sys.path:
- try:
- if os.path.isdir(dirname):
- paths.add(os.path.normcase(os.path.abspath(dirname)))
- except TypeError:
- pass
-
-extra_paths = [pythonlib]
-for name in sorted(os.listdir(pythonlib)):
- if os.path.splitext(name)[1] == '.pth':
- filename = os.path.join(pythonlib, name)
- try:
- fp = open(filename, 'rU')
- except IOError:
- continue
- try:
- for line in fp:
- if line.startswith('#'):
- continue
- if line.startswith('import'):
- exec line
- continue
- line = line.rstrip()
- path = os.path.abspath(os.path.join(pythonlib, line))
- path = os.path.normcase(path)
- if not path in paths and os.path.exists(path):
- # Here's what's different than site.py!
- extra_paths.append(path)
- paths.add(path)
- finally:
- fp.close()
-# Add the new paths to the front of sys.path
-sys.path[0:0] = extra_paths
-
-
-# Arabic and Hebrew (RFC-1556) encoding aliases. (temporary solution)
-import encodings.aliases
-encodings.aliases.aliases.update({
- 'iso_8859_6_e': 'iso8859_6',
- 'iso_8859_6_i': 'iso8859_6',
- 'iso_8859_8_e': 'iso8859_8',
- 'iso_8859_8_i': 'iso8859_8',
- })
diff --git a/Mailman/database/__init__.py b/Mailman/database/__init__.py
index 78b118be6..d332254c6 100644
--- a/Mailman/database/__init__.py
+++ b/Mailman/database/__init__.py
@@ -23,15 +23,23 @@ __all__ = [
]
import os
+import Mailman.Version
from locknix.lockfile import Lock
-from storm.properties import PropertyPublisherMeta
+from storm.locals import create_database, Store
+from string import Template
+from urlparse import urlparse
from zope.interface import implements
-from Mailman.interfaces import IDatabase
+from Mailman.Errors import SchemaVersionMismatchError
+from Mailman.configuration import config
from Mailman.database.listmanager import ListManager
-from Mailman.database.usermanager import UserManager
from Mailman.database.messagestore import MessageStore
+from Mailman.database.pending import Pendings
+from Mailman.database.requests import Requests
+from Mailman.database.usermanager import UserManager
+from Mailman.database.version import Version
+from Mailman.interfaces import IDatabase
@@ -47,46 +55,74 @@ class StockDatabase:
self._store = None
def initialize(self, debug=None):
- # Avoid circular imports.
- from Mailman.configuration import config
- from Mailman.database import model
- from Mailman.database.model import Pendings
- from Mailman.database.model import Requests
# Serialize this so we don't get multiple processes trying to create
# the database at the same time.
with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')):
- self.store = model.initialize(debug)
+ self._create(debug)
self.list_manager = ListManager()
self.user_manager = UserManager()
self.message_store = MessageStore()
self.pendings = Pendings()
self.requests = Requests()
+ def _create(self, debug):
+ # Calculate the engine url.
+ url = Template(config.DEFAULT_DATABASE_URL).safe_substitute(
+ config.paths)
+ # XXX By design of SQLite, database file creation does not honor
+ # umask. See their ticket #1193:
+ # http://www.sqlite.org/cvstrac/tktview?tn=1193,31
+ #
+ # This sucks for us because the mailman.db file /must/ be group
+ # writable, however even though we guarantee our umask is 002 here, it
+ # still gets created without the necessary g+w permission, due to
+ # SQLite's policy. This should only affect SQLite engines because its
+ # the only one that creates a little file on the local file system.
+ # This kludges around their bug by "touch"ing the database file before
+ # SQLite has any chance to create it, thus honoring the umask and
+ # ensuring the right permissions. We only try to do this for SQLite
+ # engines, and yes, we could have chmod'd the file after the fact, but
+ # half dozen and all...
+ touch(url)
+ database = create_database(url)
+ store = Store(database)
+ database.DEBUG = (config.DEFAULT_DATABASE_ECHO
+ if debug is None else debug)
+ # Storm does not currently have schema creation. This is not an ideal
+ # way to handle creating the database, but it's cheap and easy for
+ # now.
+ import Mailman.database
+ schema_file = os.path.join(
+ os.path.dirname(Mailman.database.__file__),
+ 'mailman.sql')
+ with open(schema_file) as fp:
+ sql = fp.read()
+ for statement in sql.split(';'):
+ store.execute(statement + ';')
+ # Validate schema version.
+ v = store.find(Version, component=u'schema').one()
+ if not v:
+ # Database has not yet been initialized
+ v = Version(component=u'schema',
+ version=Mailman.Version.DATABASE_SCHEMA_VERSION)
+ store.add(v)
+ elif v.version <> Mailman.Version.DATABASE_SCHEMA_VERSION:
+ # XXX Update schema
+ raise SchemaVersionMismatchError(v.version)
+ self.store = store
+
def _reset(self):
- for model_class in _class_registry:
- for row in self.store.find(model_class):
- self.store.remove(row)
+ from Mailman.database.model import ModelMeta
+ ModelMeta._reset(self.store)
-_class_registry = set()
-
-
-class ModelMeta(PropertyPublisherMeta):
- """Do more magic on table classes."""
-
- def __init__(self, name, bases, dict):
- # Before we let the base class do it's thing, force an __storm_table__
- # property to enforce our table naming convention.
- self.__storm_table__ = name.lower()
- super(ModelMeta, self).__init__(name, bases, dict)
- # Register the model class so that it can be more easily cleared.
- # This is required by the test framework.
- if name == 'Model':
- return
- _class_registry.add(self)
-
-
-class Model(object):
- """Like Storm's `Storm` subclass, but with a bit extra."""
- __metaclass__ = ModelMeta
+def touch(url):
+ parts = urlparse(url)
+ if parts.scheme <> 'sqlite':
+ return
+ path = os.path.normpath(parts.path)
+ fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666)
+ # Ignore errors
+ if fd > 0:
+ os.close(fd)
diff --git a/Mailman/database/address.py b/Mailman/database/address.py
new file mode 100644
index 000000000..a67fe41dc
--- /dev/null
+++ b/Mailman/database/address.py
@@ -0,0 +1,86 @@
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from email.utils import formataddr
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman import Errors
+from Mailman.configuration import config
+from Mailman.database.member import Member
+from Mailman.database.model import Model
+from Mailman.database.preferences import Preferences
+from Mailman.interfaces import IAddress
+
+
+
+class Address(Model):
+ implements(IAddress)
+
+ id = Int(primary=True)
+ address = Unicode()
+ _original = Unicode()
+ real_name = Unicode()
+ verified_on = DateTime()
+ registered_on = DateTime()
+
+ user_id = Int()
+ user = Reference(user_id, 'User.id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
+
+ def __init__(self, address, real_name):
+ super(Address, self).__init__()
+ lower_case = address.lower()
+ self.address = lower_case
+ self.real_name = real_name
+ self._original = (None if lower_case == address else address)
+
+ def __str__(self):
+ addr = (self.address if self._original is None else self._original)
+ return formataddr((self.real_name, addr))
+
+ def __repr__(self):
+ verified = ('verified' if self.verified_on else 'not verified')
+ address_str = str(self)
+ if self._original is None:
+ return '' % (
+ address_str, verified, id(self))
+ else:
+ return '' % (
+ address_str, verified, self.address, id(self))
+
+ def subscribe(self, mailing_list, role):
+ # This member has no preferences by default.
+ member = config.db.store.find(
+ Member,
+ Member.role == role,
+ Member.mailing_list == mailing_list.fqdn_listname,
+ Member.address == self).one()
+ if member:
+ raise Errors.AlreadySubscribedError(
+ mailing_list.fqdn_listname, self.address, role)
+ member = Member(role=role,
+ mailing_list=mailing_list.fqdn_listname,
+ address=self)
+ member.preferences = Preferences()
+ config.db.store.add(member)
+ return member
+
+ @property
+ def original_address(self):
+ return (self.address if self._original is None else self._original)
diff --git a/Mailman/database/language.py b/Mailman/database/language.py
new file mode 100644
index 000000000..a5229ab6a
--- /dev/null
+++ b/Mailman/database/language.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.database import Model
+from Mailman.interfaces import ILanguage
+
+
+
+class Language(Model):
+ implements(ILanguage)
+
+ id = Int(primary=True)
+ code = Unicode()
diff --git a/Mailman/database/listmanager.py b/Mailman/database/listmanager.py
index 48e7fe62c..01720b37c 100644
--- a/Mailman/database/listmanager.py
+++ b/Mailman/database/listmanager.py
@@ -24,6 +24,7 @@ from zope.interface import implements
from Mailman import Errors
from Mailman.Utils import split_listname, fqdn_listname
from Mailman.configuration import config
+from Mailman.database.mailinglist import MailingList
from Mailman.interfaces import IListManager
@@ -32,8 +33,6 @@ class ListManager(object):
implements(IListManager)
def create(self, fqdn_listname):
- # Avoid circular imports.
- from Mailman.database.model import MailingList
listname, hostname = split_listname(fqdn_listname)
mlist = config.db.store.find(
MailingList,
@@ -50,8 +49,6 @@ class ListManager(object):
config.db.store.remove(mlist)
def get(self, fqdn_listname):
- # Avoid circular imports.
- from Mailman.database.model import MailingList
listname, hostname = split_listname(fqdn_listname)
mlist = config.db.store.find(MailingList,
list_name=listname,
@@ -70,7 +67,5 @@ class ListManager(object):
@property
def names(self):
- # Avoid circular imports.
- from Mailman.database.model import MailingList
for mlist in config.db.store.find(MailingList):
yield fqdn_listname(mlist.list_name, mlist.host_name)
diff --git a/Mailman/database/mailinglist.py b/Mailman/database/mailinglist.py
new file mode 100644
index 000000000..04c872aab
--- /dev/null
+++ b/Mailman/database/mailinglist.py
@@ -0,0 +1,257 @@
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+import os
+import string
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.Utils import fqdn_listname, makedirs, split_listname
+from Mailman.configuration import config
+from Mailman.database import roster
+from Mailman.database.model import Model
+from Mailman.database.types import Enum
+from Mailman.interfaces import IMailingList, Personalization
+
+
+SPACE = ' '
+UNDERSCORE = '_'
+
+
+
+class MailingList(Model):
+ implements(IMailingList)
+
+ id = Int(primary=True)
+
+ # List identity
+ list_name = Unicode()
+ host_name = Unicode()
+ # Attributes not directly modifiable via the web u/i
+ created_at = DateTime()
+ web_page_url = Unicode()
+ admin_member_chunksize = Int()
+ hold_and_cmd_autoresponses = Pickle()
+ # Attributes which are directly modifiable via the web u/i. The more
+ # complicated attributes are currently stored as pickles, though that
+ # will change as the schema and implementation is developed.
+ next_request_id = Int()
+ next_digest_number = Int()
+ admin_responses = Pickle()
+ postings_responses = Pickle()
+ request_responses = Pickle()
+ digest_last_sent_at = Float()
+ one_last_digest = Pickle()
+ volume = Int()
+ last_post_time = DateTime()
+ # Attributes which are directly modifiable via the web u/i. The more
+ # complicated attributes are currently stored as pickles, though that
+ # will change as the schema and implementation is developed.
+ accept_these_nonmembers = Pickle()
+ acceptable_aliases = Pickle()
+ admin_immed_notify = Bool()
+ admin_notify_mchanges = Bool()
+ administrivia = Bool()
+ advertised = Bool()
+ anonymous_list = Bool()
+ archive = Bool()
+ archive_private = Bool()
+ archive_volume_frequency = Int()
+ autorespond_admin = Bool()
+ autorespond_postings = Bool()
+ autorespond_requests = Int()
+ autoresponse_admin_text = Unicode()
+ autoresponse_graceperiod = TimeDelta()
+ autoresponse_postings_text = Unicode()
+ autoresponse_request_text = Unicode()
+ ban_list = Pickle()
+ bounce_info_stale_after = TimeDelta()
+ bounce_matching_headers = Unicode()
+ bounce_notify_owner_on_disable = Bool()
+ bounce_notify_owner_on_removal = Bool()
+ bounce_processing = Bool()
+ bounce_score_threshold = Int()
+ bounce_unrecognized_goes_to_list_owner = Bool()
+ bounce_you_are_disabled_warnings = Int()
+ bounce_you_are_disabled_warnings_interval = TimeDelta()
+ collapse_alternatives = Bool()
+ convert_html_to_plaintext = Bool()
+ default_member_moderation = Bool()
+ description = Unicode()
+ digest_footer = Unicode()
+ digest_header = Unicode()
+ digest_is_default = Bool()
+ digest_send_periodic = Bool()
+ digest_size_threshold = Int()
+ digest_volume_frequency = Int()
+ digestable = Bool()
+ discard_these_nonmembers = Pickle()
+ emergency = Bool()
+ encode_ascii_prefixes = Bool()
+ filter_action = Int()
+ filter_content = Bool()
+ filter_filename_extensions = Pickle()
+ filter_mime_types = Pickle()
+ first_strip_reply_to = Bool()
+ forward_auto_discards = Bool()
+ gateway_to_mail = Bool()
+ gateway_to_news = Bool()
+ generic_nonmember_action = Int()
+ goodbye_msg = Unicode()
+ header_filter_rules = Pickle()
+ hold_these_nonmembers = Pickle()
+ include_list_post_header = Bool()
+ include_rfc2369_headers = Bool()
+ info = Unicode()
+ linked_newsgroup = Unicode()
+ max_days_to_hold = Int()
+ max_message_size = Int()
+ max_num_recipients = Int()
+ member_moderation_action = Enum()
+ member_moderation_notice = Unicode()
+ mime_is_default_digest = Bool()
+ moderator_password = Unicode()
+ msg_footer = Unicode()
+ msg_header = Unicode()
+ new_member_options = Int()
+ news_moderation = Enum()
+ news_prefix_subject_too = Bool()
+ nntp_host = Unicode()
+ nondigestable = Bool()
+ nonmember_rejection_notice = Unicode()
+ obscure_addresses = Bool()
+ pass_filename_extensions = Pickle()
+ pass_mime_types = Pickle()
+ personalize = Enum()
+ post_id = Int()
+ preferred_language = Unicode()
+ private_roster = Bool()
+ real_name = Unicode()
+ reject_these_nonmembers = Pickle()
+ reply_goes_to_list = Enum()
+ reply_to_address = Unicode()
+ require_explicit_destination = Bool()
+ respond_to_post_requests = Bool()
+ scrub_nondigest = Bool()
+ send_goodbye_msg = Bool()
+ send_reminders = Bool()
+ send_welcome_msg = Bool()
+ subject_prefix = Unicode()
+ subscribe_auto_approval = Pickle()
+ subscribe_policy = Int()
+ topics = Pickle()
+ topics_bodylines_limit = Int()
+ topics_enabled = Bool()
+ unsubscribe_policy = Int()
+ welcome_msg = Unicode()
+ # Relationships
+## has_and_belongs_to_many(
+## 'available_languages',
+## of_kind='Mailman.database.model.languages.Language')
+
+ def __init__(self, fqdn_listname):
+ super(MailingList, self).__init__()
+ listname, hostname = split_listname(fqdn_listname)
+ self.list_name = listname
+ self.host_name = hostname
+ # For the pending database
+ self.next_request_id = 1
+ self._restore()
+ # Max autoresponses per day. A mapping between addresses and a
+ # 2-tuple of the date of the last autoresponse and the number of
+ # autoresponses sent on that date.
+ self.hold_and_cmd_autoresponses = {}
+ self.full_path = os.path.join(config.LIST_DATA_DIR, fqdn_listname)
+ self.personalization = Personalization.none
+ self.real_name = string.capwords(
+ SPACE.join(listname.split(UNDERSCORE)))
+ makedirs(self.full_path)
+
+ # XXX FIXME
+ def _restore(self):
+ self.owners = roster.OwnerRoster(self)
+ self.moderators = roster.ModeratorRoster(self)
+ self.administrators = roster.AdministratorRoster(self)
+ self.members = roster.MemberRoster(self)
+ self.regular_members = roster.RegularMemberRoster(self)
+ self.digest_members = roster.DigestMemberRoster(self)
+ self.subscribers = roster.Subscribers(self)
+
+ @property
+ def fqdn_listname(self):
+ """See IMailingListIdentity."""
+ return fqdn_listname(self.list_name, self.host_name)
+
+ @property
+ def web_host(self):
+ """See IMailingListWeb."""
+ return config.domains[self.host_name]
+
+ def script_url(self, target, context=None):
+ """See IMailingListWeb."""
+ # XXX Handle the case for when context is not None; those would be
+ # relative URLs.
+ return self.web_page_url + target + '/' + self.fqdn_listname
+
+ # IMailingListAddresses
+
+ @property
+ def posting_address(self):
+ return self.fqdn_listname
+
+ @property
+ def noreply_address(self):
+ return '%s@%s' % (config.NO_REPLY_ADDRESS, self.host_name)
+
+ @property
+ def owner_address(self):
+ return '%s-owner@%s' % (self.list_name, self.host_name)
+
+ @property
+ def request_address(self):
+ return '%s-request@%s' % (self.list_name, self.host_name)
+
+ @property
+ def bounces_address(self):
+ return '%s-bounces@%s' % (self.list_name, self.host_name)
+
+ @property
+ def join_address(self):
+ return '%s-join@%s' % (self.list_name, self.host_name)
+
+ @property
+ def leave_address(self):
+ return '%s-leave@%s' % (self.list_name, self.host_name)
+
+ @property
+ def subscribe_address(self):
+ return '%s-subscribe@%s' % (self.list_name, self.host_name)
+
+ @property
+ def unsubscribe_address(self):
+ return '%s-unsubscribe@%s' % (self.list_name, self.host_name)
+
+ def confirm_address(self, cookie):
+ template = string.Template(config.VERP_CONFIRM_FORMAT)
+ local_part = template.safe_substitute(
+ address = '%s-confirm' % self.list_name,
+ cookie = cookie)
+ return '%s@%s' % (local_part, self.host_name)
+
+ def __repr__(self):
+ return '' % (self.fqdn_listname, id(self))
diff --git a/Mailman/database/mailman.sql b/Mailman/database/mailman.sql
new file mode 100644
index 000000000..a20b1b118
--- /dev/null
+++ b/Mailman/database/mailman.sql
@@ -0,0 +1,206 @@
+CREATE TABLE _request (
+ id INTEGER NOT NULL,
+ "key" TEXT,
+ request_type TEXT,
+ data_hash TEXT,
+ mailing_list_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id)
+);
+CREATE TABLE address (
+ id INTEGER NOT NULL,
+ address TEXT,
+ _original TEXT,
+ real_name TEXT,
+ verified_on TIMESTAMP,
+ registered_on TIMESTAMP,
+ user_id INTEGER,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id),
+ CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE language (
+ id INTEGER NOT NULL,
+ code TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE mailinglist (
+ id INTEGER NOT NULL,
+ list_name TEXT,
+ host_name TEXT,
+ created_at TIMESTAMP,
+ web_page_url TEXT,
+ admin_member_chunksize INTEGER,
+ hold_and_cmd_autoresponses BLOB,
+ next_request_id INTEGER,
+ next_digest_number INTEGER,
+ admin_responses BLOB,
+ postings_responses BLOB,
+ request_responses BLOB,
+ digest_last_sent_at NUMERIC(10, 2),
+ one_last_digest BLOB,
+ volume INTEGER,
+ last_post_time TIMESTAMP,
+ accept_these_nonmembers BLOB,
+ acceptable_aliases BLOB,
+ admin_immed_notify BOOLEAN,
+ admin_notify_mchanges BOOLEAN,
+ administrivia BOOLEAN,
+ advertised BOOLEAN,
+ anonymous_list BOOLEAN,
+ archive BOOLEAN,
+ archive_private BOOLEAN,
+ archive_volume_frequency INTEGER,
+ autorespond_admin BOOLEAN,
+ autorespond_postings BOOLEAN,
+ autorespond_requests INTEGER,
+ autoresponse_admin_text TEXT,
+ autoresponse_graceperiod TEXT,
+ autoresponse_postings_text TEXT,
+ autoresponse_request_text TEXT,
+ ban_list BLOB,
+ bounce_info_stale_after TEXT,
+ bounce_matching_headers TEXT,
+ bounce_notify_owner_on_disable BOOLEAN,
+ bounce_notify_owner_on_removal BOOLEAN,
+ bounce_processing BOOLEAN,
+ bounce_score_threshold INTEGER,
+ bounce_unrecognized_goes_to_list_owner BOOLEAN,
+ bounce_you_are_disabled_warnings INTEGER,
+ bounce_you_are_disabled_warnings_interval TEXT,
+ collapse_alternatives BOOLEAN,
+ convert_html_to_plaintext BOOLEAN,
+ default_member_moderation BOOLEAN,
+ description TEXT,
+ digest_footer TEXT,
+ digest_header TEXT,
+ digest_is_default BOOLEAN,
+ digest_send_periodic BOOLEAN,
+ digest_size_threshold INTEGER,
+ digest_volume_frequency INTEGER,
+ digestable BOOLEAN,
+ discard_these_nonmembers BLOB,
+ emergency BOOLEAN,
+ encode_ascii_prefixes BOOLEAN,
+ filter_action INTEGER,
+ filter_content BOOLEAN,
+ filter_filename_extensions BLOB,
+ filter_mime_types BLOB,
+ first_strip_reply_to BOOLEAN,
+ forward_auto_discards BOOLEAN,
+ gateway_to_mail BOOLEAN,
+ gateway_to_news BOOLEAN,
+ generic_nonmember_action INTEGER,
+ goodbye_msg TEXT,
+ header_filter_rules BLOB,
+ hold_these_nonmembers BLOB,
+ include_list_post_header BOOLEAN,
+ include_rfc2369_headers BOOLEAN,
+ info TEXT,
+ linked_newsgroup TEXT,
+ max_days_to_hold INTEGER,
+ max_message_size INTEGER,
+ max_num_recipients INTEGER,
+ member_moderation_action BOOLEAN,
+ member_moderation_notice TEXT,
+ mime_is_default_digest BOOLEAN,
+ moderator_password TEXT,
+ msg_footer TEXT,
+ msg_header TEXT,
+ new_member_options INTEGER,
+ news_moderation TEXT,
+ news_prefix_subject_too BOOLEAN,
+ nntp_host TEXT,
+ nondigestable BOOLEAN,
+ nonmember_rejection_notice TEXT,
+ obscure_addresses BOOLEAN,
+ pass_filename_extensions BLOB,
+ pass_mime_types BLOB,
+ personalize TEXT,
+ post_id INTEGER,
+ preferred_language TEXT,
+ private_roster BOOLEAN,
+ real_name TEXT,
+ reject_these_nonmembers BLOB,
+ reply_goes_to_list TEXT,
+ reply_to_address TEXT,
+ require_explicit_destination BOOLEAN,
+ respond_to_post_requests BOOLEAN,
+ scrub_nondigest BOOLEAN,
+ send_goodbye_msg BOOLEAN,
+ send_reminders BOOLEAN,
+ send_welcome_msg BOOLEAN,
+ subject_prefix TEXT,
+ subscribe_auto_approval BLOB,
+ subscribe_policy INTEGER,
+ topics BLOB,
+ topics_bodylines_limit INTEGER,
+ topics_enabled BOOLEAN,
+ unsubscribe_policy INTEGER,
+ welcome_msg TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE member (
+ id INTEGER NOT NULL,
+ role TEXT,
+ mailing_list TEXT,
+ address_id INTEGER,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id),
+ CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE message (
+ id INTEGER NOT NULL,
+ hash TEXT,
+ path TEXT,
+ message_id TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE pended (
+ id INTEGER NOT NULL,
+ token TEXT,
+ expiration_date TIMESTAMP,
+ PRIMARY KEY (id)
+);
+CREATE TABLE pendedkeyvalue (
+ id INTEGER NOT NULL,
+ "key" TEXT,
+ value TEXT,
+ pended_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id)
+);
+CREATE TABLE preferences (
+ id INTEGER NOT NULL,
+ acknowledge_posts BOOLEAN,
+ hide_address BOOLEAN,
+ preferred_language TEXT,
+ receive_list_copy BOOLEAN,
+ receive_own_postings BOOLEAN,
+ delivery_mode TEXT,
+ delivery_status TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE user (
+ id INTEGER NOT NULL,
+ real_name TEXT,
+ password TEXT,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT user_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE version (
+ id INTEGER NOT NULL,
+ component TEXT,
+ version INTEGER,
+ PRIMARY KEY (id)
+);
+CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
+CREATE INDEX ix_address_preferences_id ON address (preferences_id);
+CREATE INDEX ix_address_user_id ON address (user_id);
+CREATE INDEX ix_member_address_id ON member (address_id);
+CREATE INDEX ix_member_preferences_id ON member (preferences_id);
+CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
+CREATE INDEX ix_user_preferences_id ON user (preferences_id);
diff --git a/Mailman/database/member.py b/Mailman/database/member.py
new file mode 100644
index 000000000..f77b8c7c3
--- /dev/null
+++ b/Mailman/database/member.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.Utils import split_listname
+from Mailman.configuration import config
+from Mailman.constants import SystemDefaultPreferences
+from Mailman.database.model import Model
+from Mailman.database.types import Enum
+from Mailman.interfaces import IMember, IPreferences
+
+
+
+class Member(Model):
+ implements(IMember)
+
+ id = Int(primary=True)
+ role = Enum()
+ mailing_list = Unicode()
+
+ address_id = Int()
+ address = Reference(address_id, 'Address.id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
+
+ def __init__(self, role, mailing_list, address):
+ self.role = role
+ self.mailing_list = mailing_list
+ self.address = address
+
+ def __repr__(self):
+ return '' % (
+ self.address, self.mailing_list, self.role)
+
+ def _lookup(self, preference):
+ pref = getattr(self.preferences, preference)
+ if pref is not None:
+ return pref
+ pref = getattr(self.address.preferences, preference)
+ if pref is not None:
+ return pref
+ if self.address.user:
+ pref = getattr(self.address.user.preferences, preference)
+ if pref is not None:
+ return pref
+ return getattr(SystemDefaultPreferences, preference)
+
+ @property
+ def acknowledge_posts(self):
+ return self._lookup('acknowledge_posts')
+
+ @property
+ def preferred_language(self):
+ return self._lookup('preferred_language')
+
+ @property
+ def receive_list_copy(self):
+ return self._lookup('receive_list_copy')
+
+ @property
+ def receive_own_postings(self):
+ return self._lookup('receive_own_postings')
+
+ @property
+ def delivery_mode(self):
+ return self._lookup('delivery_mode')
+
+ @property
+ def delivery_status(self):
+ return self._lookup('delivery_status')
+
+ @property
+ def options_url(self):
+ # XXX Um, this is definitely wrong
+ return 'http://example.com/' + self.address.address
+
+ def unsubscribe(self):
+ config.db.store.remove(self.preferences)
+ config.db.store.remove(self)
diff --git a/Mailman/database/message.py b/Mailman/database/message.py
new file mode 100644
index 000000000..b8a7e3dfb
--- /dev/null
+++ b/Mailman/database/message.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.database.model import Model
+from Mailman.interfaces import IMessage
+
+
+
+class Message(Model):
+ """A message in the message store."""
+
+ implements(IMessage)
+
+ id = Int(primary=True, default=AutoReload)
+ message_id = Unicode()
+ hash = RawStr()
+ path = RawStr()
+ # This is a Messge-ID field representation, not a database row id.
+
+ def __init__(self, message_id, hash, path):
+ self.message_id = message_id
+ self.hash = hash
+ self.path = path
+ config.db.store.add(self)
diff --git a/Mailman/database/messagestore.py b/Mailman/database/messagestore.py
index 7c90918ac..69c5d58d5 100644
--- a/Mailman/database/messagestore.py
+++ b/Mailman/database/messagestore.py
@@ -32,6 +32,7 @@ from zope.interface import implements
from Mailman import Utils
from Mailman.configuration import config
+from Mailman.database.message import Message
from Mailman.interfaces import IMessageStore
# It could be very bad if you have already stored files and you change this
@@ -45,7 +46,6 @@ class MessageStore:
implements(IMessageStore)
def add(self, message):
- from Mailman.database.model import Message
# Ensure that the message has the requisite headers.
message_ids = message.get_all('message-id', [])
if len(message_ids) <> 1:
@@ -96,14 +96,10 @@ class MessageStore:
return pickle.load(fp)
def get_messages_by_message_id(self, message_id):
- # Avoid circular imports.
- from Mailman.database.model.message import Message
for msgrow in config.db.store.find(Message, message_id=message_id):
yield self._msgobj(msgrow)
def get_messages_by_hash(self, hash):
- # Avoid circular imports.
- from Mailman.database.model.message import Message
# It's possible the hash came from a message header, in which case it
# will be a Unicode. However when coming from source code, it will
# always be an 8-string. Coerce to the latter if necessary; it must
@@ -119,8 +115,6 @@ class MessageStore:
seqno = int(seqno)
except ValueError:
return None
- # Avoid circular imports.
- from Mailman.database.model.message import Message
messages = config.db.store.find(Message, id=seqno)
if messages.count() == 0:
return None
@@ -137,8 +131,6 @@ class MessageStore:
@property
def messages(self):
- # Avoid circular imports.
- from Mailman.database.model.message import Message
for msgrow in config.db.store.find(Message):
yield self._msgobj(msgrow)
diff --git a/Mailman/database/model.py b/Mailman/database/model.py
new file mode 100644
index 000000000..3b41af123
--- /dev/null
+++ b/Mailman/database/model.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Base class for all database classes."""
+
+__metaclass__ = type
+__all__ = [
+ 'Model',
+ ]
+
+from storm.properties import PropertyPublisherMeta
+
+
+
+class ModelMeta(PropertyPublisherMeta):
+ """Do more magic on table classes."""
+
+ _class_registry = set()
+
+ def __init__(self, name, bases, dict):
+ # Before we let the base class do it's thing, force an __storm_table__
+ # property to enforce our table naming convention.
+ self.__storm_table__ = name.lower()
+ super(ModelMeta, self).__init__(name, bases, dict)
+ # Register the model class so that it can be more easily cleared.
+ # This is required by the test framework.
+ if name == 'Model':
+ return
+ ModelMeta._class_registry.add(self)
+
+ @staticmethod
+ def _reset(store):
+ for model_class in ModelMeta._class_registry:
+ for row in store.find(model_class):
+ store.remove(row)
+
+
+
+class Model(object):
+ """Like Storm's `Storm` subclass, but with a bit extra."""
+ __metaclass__ = ModelMeta
diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py
deleted file mode 100644
index c15670403..000000000
--- a/Mailman/database/model/__init__.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from __future__ import with_statement
-
-__all__ = [
- 'Address',
- 'Language',
- 'MailingList',
- 'Message',
- 'Pendings',
- 'Preferences',
- 'User',
- 'Version',
- ]
-
-import os
-import sys
-
-from storm import database
-from storm.locals import create_database, Store
-from string import Template
-from urlparse import urlparse
-
-import Mailman.Version
-
-from Mailman import constants
-from Mailman.Errors import SchemaVersionMismatchError
-from Mailman.configuration import config
-from Mailman.database.model.address import Address
-from Mailman.database.model.language import Language
-from Mailman.database.model.mailinglist import MailingList
-from Mailman.database.model.member import Member
-from Mailman.database.model.message import Message
-from Mailman.database.model.pending import Pendings
-from Mailman.database.model.preferences import Preferences
-from Mailman.database.model.requests import Requests
-from Mailman.database.model.user import User
-from Mailman.database.model.version import Version
-
-
-
-def initialize(debug):
- # Calculate the engine url.
- url = Template(config.DEFAULT_DATABASE_URL).safe_substitute(config.paths)
- # XXX By design of SQLite, database file creation does not honor
- # umask. See their ticket #1193:
- # http://www.sqlite.org/cvstrac/tktview?tn=1193,31
- #
- # This sucks for us because the mailman.db file /must/ be group writable,
- # however even though we guarantee our umask is 002 here, it still gets
- # created without the necessary g+w permission, due to SQLite's policy.
- # This should only affect SQLite engines because its the only one that
- # creates a little file on the local file system. This kludges around
- # their bug by "touch"ing the database file before SQLite has any chance
- # to create it, thus honoring the umask and ensuring the right
- # permissions. We only try to do this for SQLite engines, and yes, we
- # could have chmod'd the file after the fact, but half dozen and all...
- touch(url)
- database = create_database(url)
- store = Store(database)
- database.DEBUG = (config.DEFAULT_DATABASE_ECHO if debug is None else debug)
- # XXX Storm does not currently have schema creation. This is not an ideal
- # way to handle creating the database, but it's cheap and easy for now.
- import Mailman.database.model
- schema_file = os.path.join(
- os.path.dirname(Mailman.database.model.__file__),
- 'mailman.sql')
- with open(schema_file) as fp:
- sql = fp.read()
- for statement in sql.split(';'):
- store.execute(statement + ';')
- # Validate schema version.
- v = store.find(Version, component=u'schema').one()
- if not v:
- # Database has not yet been initialized
- v = Version(component=u'schema',
- version=Mailman.Version.DATABASE_SCHEMA_VERSION)
- store.add(v)
- elif v.version <> Mailman.Version.DATABASE_SCHEMA_VERSION:
- # XXX Update schema
- raise SchemaVersionMismatchError(v.version)
- return store
-
-
-def touch(url):
- parts = urlparse(url)
- if parts.scheme <> 'sqlite':
- return
- path = os.path.normpath(parts.path)
- fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666)
- # Ignore errors
- if fd > 0:
- os.close(fd)
diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py
deleted file mode 100644
index b8e2f0f31..000000000
--- a/Mailman/database/model/address.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from email.utils import formataddr
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman import Errors
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.interfaces import IAddress
-
-
-
-class Address(Model):
- implements(IAddress)
-
- id = Int(primary=True)
- address = Unicode()
- _original = Unicode()
- real_name = Unicode()
- verified_on = DateTime()
- registered_on = DateTime()
-
- user_id = Int()
- user = Reference(user_id, 'User.id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
-
- def __init__(self, address, real_name):
- super(Address, self).__init__()
- lower_case = address.lower()
- self.address = lower_case
- self.real_name = real_name
- self._original = (None if lower_case == address else address)
-
- def __str__(self):
- addr = (self.address if self._original is None else self._original)
- return formataddr((self.real_name, addr))
-
- def __repr__(self):
- verified = ('verified' if self.verified_on else 'not verified')
- address_str = str(self)
- if self._original is None:
- return '' % (
- address_str, verified, id(self))
- else:
- return '' % (
- address_str, verified, self.address, id(self))
-
- def subscribe(self, mailing_list, role):
- from Mailman.database.model import Member
- from Mailman.database.model import Preferences
- # This member has no preferences by default.
- member = config.db.store.find(
- Member,
- Member.role == role,
- Member.mailing_list == mailing_list.fqdn_listname,
- Member.address == self).one()
- if member:
- raise Errors.AlreadySubscribedError(
- mailing_list.fqdn_listname, self.address, role)
- member = Member(role=role,
- mailing_list=mailing_list.fqdn_listname,
- address=self)
- member.preferences = Preferences()
- config.db.store.add(member)
- return member
-
- @property
- def original_address(self):
- return (self.address if self._original is None else self._original)
diff --git a/Mailman/database/model/language.py b/Mailman/database/model/language.py
deleted file mode 100644
index a5229ab6a..000000000
--- a/Mailman/database/model/language.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.database import Model
-from Mailman.interfaces import ILanguage
-
-
-
-class Language(Model):
- implements(ILanguage)
-
- id = Int(primary=True)
- code = Unicode()
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
deleted file mode 100644
index 3a3396758..000000000
--- a/Mailman/database/model/mailinglist.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-import os
-import string
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.Utils import fqdn_listname, makedirs, split_listname
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.database.types import Enum
-from Mailman.interfaces import IMailingList, Personalization
-
-SPACE = ' '
-UNDERSCORE = '_'
-
-
-
-class MailingList(Model):
- implements(IMailingList)
-
- id = Int(primary=True)
-
- # List identity
- list_name = Unicode()
- host_name = Unicode()
- # Attributes not directly modifiable via the web u/i
- created_at = DateTime()
- web_page_url = Unicode()
- admin_member_chunksize = Int()
- hold_and_cmd_autoresponses = Pickle()
- # Attributes which are directly modifiable via the web u/i. The more
- # complicated attributes are currently stored as pickles, though that
- # will change as the schema and implementation is developed.
- next_request_id = Int()
- next_digest_number = Int()
- admin_responses = Pickle()
- postings_responses = Pickle()
- request_responses = Pickle()
- digest_last_sent_at = Float()
- one_last_digest = Pickle()
- volume = Int()
- last_post_time = DateTime()
- # Attributes which are directly modifiable via the web u/i. The more
- # complicated attributes are currently stored as pickles, though that
- # will change as the schema and implementation is developed.
- accept_these_nonmembers = Pickle()
- acceptable_aliases = Pickle()
- admin_immed_notify = Bool()
- admin_notify_mchanges = Bool()
- administrivia = Bool()
- advertised = Bool()
- anonymous_list = Bool()
- archive = Bool()
- archive_private = Bool()
- archive_volume_frequency = Int()
- autorespond_admin = Bool()
- autorespond_postings = Bool()
- autorespond_requests = Int()
- autoresponse_admin_text = Unicode()
- autoresponse_graceperiod = TimeDelta()
- autoresponse_postings_text = Unicode()
- autoresponse_request_text = Unicode()
- ban_list = Pickle()
- bounce_info_stale_after = TimeDelta()
- bounce_matching_headers = Unicode()
- bounce_notify_owner_on_disable = Bool()
- bounce_notify_owner_on_removal = Bool()
- bounce_processing = Bool()
- bounce_score_threshold = Int()
- bounce_unrecognized_goes_to_list_owner = Bool()
- bounce_you_are_disabled_warnings = Int()
- bounce_you_are_disabled_warnings_interval = TimeDelta()
- collapse_alternatives = Bool()
- convert_html_to_plaintext = Bool()
- default_member_moderation = Bool()
- description = Unicode()
- digest_footer = Unicode()
- digest_header = Unicode()
- digest_is_default = Bool()
- digest_send_periodic = Bool()
- digest_size_threshold = Int()
- digest_volume_frequency = Int()
- digestable = Bool()
- discard_these_nonmembers = Pickle()
- emergency = Bool()
- encode_ascii_prefixes = Bool()
- filter_action = Int()
- filter_content = Bool()
- filter_filename_extensions = Pickle()
- filter_mime_types = Pickle()
- first_strip_reply_to = Bool()
- forward_auto_discards = Bool()
- gateway_to_mail = Bool()
- gateway_to_news = Bool()
- generic_nonmember_action = Int()
- goodbye_msg = Unicode()
- header_filter_rules = Pickle()
- hold_these_nonmembers = Pickle()
- include_list_post_header = Bool()
- include_rfc2369_headers = Bool()
- info = Unicode()
- linked_newsgroup = Unicode()
- max_days_to_hold = Int()
- max_message_size = Int()
- max_num_recipients = Int()
- member_moderation_action = Enum()
- member_moderation_notice = Unicode()
- mime_is_default_digest = Bool()
- moderator_password = Unicode()
- msg_footer = Unicode()
- msg_header = Unicode()
- new_member_options = Int()
- news_moderation = Enum()
- news_prefix_subject_too = Bool()
- nntp_host = Unicode()
- nondigestable = Bool()
- nonmember_rejection_notice = Unicode()
- obscure_addresses = Bool()
- pass_filename_extensions = Pickle()
- pass_mime_types = Pickle()
- personalize = Enum()
- post_id = Int()
- preferred_language = Unicode()
- private_roster = Bool()
- real_name = Unicode()
- reject_these_nonmembers = Pickle()
- reply_goes_to_list = Enum()
- reply_to_address = Unicode()
- require_explicit_destination = Bool()
- respond_to_post_requests = Bool()
- scrub_nondigest = Bool()
- send_goodbye_msg = Bool()
- send_reminders = Bool()
- send_welcome_msg = Bool()
- subject_prefix = Unicode()
- subscribe_auto_approval = Pickle()
- subscribe_policy = Int()
- topics = Pickle()
- topics_bodylines_limit = Int()
- topics_enabled = Bool()
- unsubscribe_policy = Int()
- welcome_msg = Unicode()
- # Relationships
-## has_and_belongs_to_many(
-## 'available_languages',
-## of_kind='Mailman.database.model.languages.Language')
-
- def __init__(self, fqdn_listname):
- super(MailingList, self).__init__()
- listname, hostname = split_listname(fqdn_listname)
- self.list_name = listname
- self.host_name = hostname
- # For the pending database
- self.next_request_id = 1
- self._restore()
- # Max autoresponses per day. A mapping between addresses and a
- # 2-tuple of the date of the last autoresponse and the number of
- # autoresponses sent on that date.
- self.hold_and_cmd_autoresponses = {}
- self.full_path = os.path.join(config.LIST_DATA_DIR, fqdn_listname)
- self.personalization = Personalization.none
- self.real_name = string.capwords(
- SPACE.join(listname.split(UNDERSCORE)))
- makedirs(self.full_path)
-
- # XXX FIXME
- def _restore(self):
- # Avoid circular imports.
- from Mailman.database.model import roster
- self.owners = roster.OwnerRoster(self)
- self.moderators = roster.ModeratorRoster(self)
- self.administrators = roster.AdministratorRoster(self)
- self.members = roster.MemberRoster(self)
- self.regular_members = roster.RegularMemberRoster(self)
- self.digest_members = roster.DigestMemberRoster(self)
- self.subscribers = roster.Subscribers(self)
-
- @property
- def fqdn_listname(self):
- """See IMailingListIdentity."""
- return fqdn_listname(self.list_name, self.host_name)
-
- @property
- def web_host(self):
- """See IMailingListWeb."""
- return config.domains[self.host_name]
-
- def script_url(self, target, context=None):
- """See IMailingListWeb."""
- # XXX Handle the case for when context is not None; those would be
- # relative URLs.
- return self.web_page_url + target + '/' + self.fqdn_listname
-
- # IMailingListAddresses
-
- @property
- def posting_address(self):
- return self.fqdn_listname
-
- @property
- def noreply_address(self):
- return '%s@%s' % (config.NO_REPLY_ADDRESS, self.host_name)
-
- @property
- def owner_address(self):
- return '%s-owner@%s' % (self.list_name, self.host_name)
-
- @property
- def request_address(self):
- return '%s-request@%s' % (self.list_name, self.host_name)
-
- @property
- def bounces_address(self):
- return '%s-bounces@%s' % (self.list_name, self.host_name)
-
- @property
- def join_address(self):
- return '%s-join@%s' % (self.list_name, self.host_name)
-
- @property
- def leave_address(self):
- return '%s-leave@%s' % (self.list_name, self.host_name)
-
- @property
- def subscribe_address(self):
- return '%s-subscribe@%s' % (self.list_name, self.host_name)
-
- @property
- def unsubscribe_address(self):
- return '%s-unsubscribe@%s' % (self.list_name, self.host_name)
-
- def confirm_address(self, cookie):
- template = string.Template(config.VERP_CONFIRM_FORMAT)
- local_part = template.safe_substitute(
- address = '%s-confirm' % self.list_name,
- cookie = cookie)
- return '%s@%s' % (local_part, self.host_name)
-
- def __repr__(self):
- return '' % (self.fqdn_listname, id(self))
diff --git a/Mailman/database/model/mailman.sql b/Mailman/database/model/mailman.sql
deleted file mode 100644
index a20b1b118..000000000
--- a/Mailman/database/model/mailman.sql
+++ /dev/null
@@ -1,206 +0,0 @@
-CREATE TABLE _request (
- id INTEGER NOT NULL,
- "key" TEXT,
- request_type TEXT,
- data_hash TEXT,
- mailing_list_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id)
-);
-CREATE TABLE address (
- id INTEGER NOT NULL,
- address TEXT,
- _original TEXT,
- real_name TEXT,
- verified_on TIMESTAMP,
- registered_on TIMESTAMP,
- user_id INTEGER,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id),
- CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE language (
- id INTEGER NOT NULL,
- code TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE mailinglist (
- id INTEGER NOT NULL,
- list_name TEXT,
- host_name TEXT,
- created_at TIMESTAMP,
- web_page_url TEXT,
- admin_member_chunksize INTEGER,
- hold_and_cmd_autoresponses BLOB,
- next_request_id INTEGER,
- next_digest_number INTEGER,
- admin_responses BLOB,
- postings_responses BLOB,
- request_responses BLOB,
- digest_last_sent_at NUMERIC(10, 2),
- one_last_digest BLOB,
- volume INTEGER,
- last_post_time TIMESTAMP,
- accept_these_nonmembers BLOB,
- acceptable_aliases BLOB,
- admin_immed_notify BOOLEAN,
- admin_notify_mchanges BOOLEAN,
- administrivia BOOLEAN,
- advertised BOOLEAN,
- anonymous_list BOOLEAN,
- archive BOOLEAN,
- archive_private BOOLEAN,
- archive_volume_frequency INTEGER,
- autorespond_admin BOOLEAN,
- autorespond_postings BOOLEAN,
- autorespond_requests INTEGER,
- autoresponse_admin_text TEXT,
- autoresponse_graceperiod TEXT,
- autoresponse_postings_text TEXT,
- autoresponse_request_text TEXT,
- ban_list BLOB,
- bounce_info_stale_after TEXT,
- bounce_matching_headers TEXT,
- bounce_notify_owner_on_disable BOOLEAN,
- bounce_notify_owner_on_removal BOOLEAN,
- bounce_processing BOOLEAN,
- bounce_score_threshold INTEGER,
- bounce_unrecognized_goes_to_list_owner BOOLEAN,
- bounce_you_are_disabled_warnings INTEGER,
- bounce_you_are_disabled_warnings_interval TEXT,
- collapse_alternatives BOOLEAN,
- convert_html_to_plaintext BOOLEAN,
- default_member_moderation BOOLEAN,
- description TEXT,
- digest_footer TEXT,
- digest_header TEXT,
- digest_is_default BOOLEAN,
- digest_send_periodic BOOLEAN,
- digest_size_threshold INTEGER,
- digest_volume_frequency INTEGER,
- digestable BOOLEAN,
- discard_these_nonmembers BLOB,
- emergency BOOLEAN,
- encode_ascii_prefixes BOOLEAN,
- filter_action INTEGER,
- filter_content BOOLEAN,
- filter_filename_extensions BLOB,
- filter_mime_types BLOB,
- first_strip_reply_to BOOLEAN,
- forward_auto_discards BOOLEAN,
- gateway_to_mail BOOLEAN,
- gateway_to_news BOOLEAN,
- generic_nonmember_action INTEGER,
- goodbye_msg TEXT,
- header_filter_rules BLOB,
- hold_these_nonmembers BLOB,
- include_list_post_header BOOLEAN,
- include_rfc2369_headers BOOLEAN,
- info TEXT,
- linked_newsgroup TEXT,
- max_days_to_hold INTEGER,
- max_message_size INTEGER,
- max_num_recipients INTEGER,
- member_moderation_action BOOLEAN,
- member_moderation_notice TEXT,
- mime_is_default_digest BOOLEAN,
- moderator_password TEXT,
- msg_footer TEXT,
- msg_header TEXT,
- new_member_options INTEGER,
- news_moderation TEXT,
- news_prefix_subject_too BOOLEAN,
- nntp_host TEXT,
- nondigestable BOOLEAN,
- nonmember_rejection_notice TEXT,
- obscure_addresses BOOLEAN,
- pass_filename_extensions BLOB,
- pass_mime_types BLOB,
- personalize TEXT,
- post_id INTEGER,
- preferred_language TEXT,
- private_roster BOOLEAN,
- real_name TEXT,
- reject_these_nonmembers BLOB,
- reply_goes_to_list TEXT,
- reply_to_address TEXT,
- require_explicit_destination BOOLEAN,
- respond_to_post_requests BOOLEAN,
- scrub_nondigest BOOLEAN,
- send_goodbye_msg BOOLEAN,
- send_reminders BOOLEAN,
- send_welcome_msg BOOLEAN,
- subject_prefix TEXT,
- subscribe_auto_approval BLOB,
- subscribe_policy INTEGER,
- topics BLOB,
- topics_bodylines_limit INTEGER,
- topics_enabled BOOLEAN,
- unsubscribe_policy INTEGER,
- welcome_msg TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE member (
- id INTEGER NOT NULL,
- role TEXT,
- mailing_list TEXT,
- address_id INTEGER,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id),
- CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE message (
- id INTEGER NOT NULL,
- hash TEXT,
- path TEXT,
- message_id TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE pended (
- id INTEGER NOT NULL,
- token TEXT,
- expiration_date TIMESTAMP,
- PRIMARY KEY (id)
-);
-CREATE TABLE pendedkeyvalue (
- id INTEGER NOT NULL,
- "key" TEXT,
- value TEXT,
- pended_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id)
-);
-CREATE TABLE preferences (
- id INTEGER NOT NULL,
- acknowledge_posts BOOLEAN,
- hide_address BOOLEAN,
- preferred_language TEXT,
- receive_list_copy BOOLEAN,
- receive_own_postings BOOLEAN,
- delivery_mode TEXT,
- delivery_status TEXT,
- PRIMARY KEY (id)
-);
-CREATE TABLE user (
- id INTEGER NOT NULL,
- real_name TEXT,
- password TEXT,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT user_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
-);
-CREATE TABLE version (
- id INTEGER NOT NULL,
- component TEXT,
- version INTEGER,
- PRIMARY KEY (id)
-);
-CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
-CREATE INDEX ix_address_preferences_id ON address (preferences_id);
-CREATE INDEX ix_address_user_id ON address (user_id);
-CREATE INDEX ix_member_address_id ON member (address_id);
-CREATE INDEX ix_member_preferences_id ON member (preferences_id);
-CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
-CREATE INDEX ix_user_preferences_id ON user (preferences_id);
diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py
deleted file mode 100644
index 3f9775d3c..000000000
--- a/Mailman/database/model/member.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.Utils import split_listname
-from Mailman.configuration import config
-from Mailman.constants import SystemDefaultPreferences
-from Mailman.database import Model
-from Mailman.database.types import Enum
-from Mailman.interfaces import IMember, IPreferences
-
-
-
-class Member(Model):
- implements(IMember)
-
- id = Int(primary=True)
- role = Enum()
- mailing_list = Unicode()
-
- address_id = Int()
- address = Reference(address_id, 'Address.id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
-
- def __init__(self, role, mailing_list, address):
- self.role = role
- self.mailing_list = mailing_list
- self.address = address
-
- def __repr__(self):
- return '' % (
- self.address, self.mailing_list, self.role)
-
- def _lookup(self, preference):
- pref = getattr(self.preferences, preference)
- if pref is not None:
- return pref
- pref = getattr(self.address.preferences, preference)
- if pref is not None:
- return pref
- if self.address.user:
- pref = getattr(self.address.user.preferences, preference)
- if pref is not None:
- return pref
- return getattr(SystemDefaultPreferences, preference)
-
- @property
- def acknowledge_posts(self):
- return self._lookup('acknowledge_posts')
-
- @property
- def preferred_language(self):
- return self._lookup('preferred_language')
-
- @property
- def receive_list_copy(self):
- return self._lookup('receive_list_copy')
-
- @property
- def receive_own_postings(self):
- return self._lookup('receive_own_postings')
-
- @property
- def delivery_mode(self):
- return self._lookup('delivery_mode')
-
- @property
- def delivery_status(self):
- return self._lookup('delivery_status')
-
- @property
- def options_url(self):
- # XXX Um, this is definitely wrong
- return 'http://example.com/' + self.address.address
-
- def unsubscribe(self):
- config.db.store.remove(self.preferences)
- config.db.store.remove(self)
diff --git a/Mailman/database/model/message.py b/Mailman/database/model/message.py
deleted file mode 100644
index c4ea4a636..000000000
--- a/Mailman/database/model/message.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.interfaces import IMessage
-
-
-
-class Message(Model):
- """A message in the message store."""
-
- implements(IMessage)
-
- id = Int(primary=True, default=AutoReload)
- message_id = Unicode()
- hash = RawStr()
- path = RawStr()
- # This is a Messge-ID field representation, not a database row id.
-
- def __init__(self, message_id, hash, path):
- self.message_id = message_id
- self.hash = hash
- self.path = path
- config.db.store.add(self)
diff --git a/Mailman/database/model/pending.py b/Mailman/database/model/pending.py
deleted file mode 100644
index 3f3e2caa0..000000000
--- a/Mailman/database/model/pending.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Implementations of the IPendable and IPending interfaces."""
-
-import sys
-import time
-import random
-import hashlib
-import datetime
-
-from storm.locals import *
-from zope.interface import implements
-from zope.interface.verify import verifyObject
-
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.interfaces import (
- IPendable, IPended, IPendedKeyValue, IPendings)
-
-
-
-class PendedKeyValue(Model):
- """A pended key/value pair, tied to a token."""
-
- implements(IPendedKeyValue)
-
- def __init__(self, key, value):
- self.key = key
- self.value = value
-
- id = Int(primary=True)
- key = Unicode()
- value = Unicode()
- pended_id = Int()
-
-
-class Pended(Model):
- """A pended event, tied to a token."""
-
- implements(IPended)
-
- def __init__(self, token, expiration_date):
- self.token = token
- self.expiration_date = expiration_date
-
- id = Int(primary=True)
- token = RawStr()
- expiration_date = DateTime()
- key_values = ReferenceSet(id, PendedKeyValue.pended_id)
-
-
-
-class UnpendedPendable(dict):
- implements(IPendable)
-
-
-
-class Pendings(object):
- """Implementation of the IPending interface."""
-
- implements(IPendings)
-
- def add(self, pendable, lifetime=None):
- verifyObject(IPendable, pendable)
- # Calculate the token and the lifetime.
- if lifetime is None:
- lifetime = config.PENDING_REQUEST_LIFE
- # Calculate a unique token. Algorithm vetted by the Timbot. time()
- # has high resolution on Linux, clock() on Windows. random gives us
- # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and
- # clock values basically help obscure the random number generator, as
- # does the hash calculation. The integral parts of the time values
- # are discarded because they're the most predictable bits.
- for attempts in range(3):
- now = time.time()
- x = random.random() + now % 1.0 + time.clock() % 1.0
- # Use sha1 because it produces shorter strings.
- token = hashlib.sha1(repr(x)).hexdigest()
- # In practice, we'll never get a duplicate, but we'll be anal
- # about checking anyway.
- if config.db.store.find(Pended, token=token).count() == 0:
- break
- else:
- raise AssertionError('Could not find a valid pendings token')
- # Create the record, and then the individual key/value pairs.
- pending = Pended(
- token=token,
- expiration_date=datetime.datetime.now() + lifetime)
- for key, value in pendable.items():
- if isinstance(key, str):
- key = unicode(key, 'utf-8')
- if isinstance(value, str):
- value = unicode(value, 'utf-8')
- elif type(value) is int:
- value = u'__builtin__.int\1%s' % value
- elif type(value) is float:
- value = u'__builtin__.float\1%s' % value
- elif type(value) is bool:
- value = u'__builtin__.bool\1%s' % value
- keyval = PendedKeyValue(key=key, value=value)
- pending.key_values.add(keyval)
- config.db.store.add(pending)
- return token
-
- def confirm(self, token, expunge=True):
- store = config.db.store
- pendings = store.find(Pended, token=token)
- if pendings.count() == 0:
- return None
- assert pendings.count() == 1, (
- 'Unexpected token count: %d' % pendings.count())
- pending = pendings[0]
- pendable = UnpendedPendable()
- # Find all PendedKeyValue entries that are associated with the pending
- # object's ID. Watch out for type conversions.
- for keyvalue in store.find(PendedKeyValue,
- PendedKeyValue.pended_id == pending.id):
- if keyvalue.value is not None and '\1' in keyvalue.value:
- typename, value = keyvalue.value.split('\1', 1)
- package, classname = typename.rsplit('.', 1)
- __import__(package)
- module = sys.modules[package]
- pendable[keyvalue.key] = getattr(module, classname)(value)
- else:
- pendable[keyvalue.key] = keyvalue.value
- if expunge:
- store.remove(keyvalue)
- if expunge:
- store.remove(pending)
- return pendable
-
- def evict(self):
- store = config.db.store
- now = datetime.datetime.now()
- for pending in store.find(Pended):
- if pending.expiration_date < now:
- # Find all PendedKeyValue entries that are associated with the
- # pending object's ID.
- q = store.find(PendedKeyValue,
- PendedKeyValue.pended_id == pending.id)
- for keyvalue in q:
- store.remove(keyvalue)
- store.remove(pending)
diff --git a/Mailman/database/model/preferences.py b/Mailman/database/model/preferences.py
deleted file mode 100644
index 65d909bd0..000000000
--- a/Mailman/database/model/preferences.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.database import Model
-from Mailman.database.types import Enum
-from Mailman.interfaces import IPreferences
-
-
-
-class Preferences(Model):
- implements(IPreferences)
-
- id = Int(primary=True)
- acknowledge_posts = Bool()
- hide_address = Bool()
- preferred_language = Unicode()
- receive_list_copy = Bool()
- receive_own_postings = Bool()
- delivery_mode = Enum()
- delivery_status = Enum()
-
- def __repr__(self):
- return '' % id(self)
diff --git a/Mailman/database/model/requests.py b/Mailman/database/model/requests.py
deleted file mode 100644
index 64fff7c48..000000000
--- a/Mailman/database/model/requests.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Implementations of the IRequests and IListRequests interfaces."""
-
-from datetime import timedelta
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.database.types import Enum
-from Mailman.interfaces import IListRequests, IPendable, IRequests, RequestType
-
-
-__metaclass__ = type
-__all__ = [
- 'Requests',
- ]
-
-
-
-class DataPendable(dict):
- implements(IPendable)
-
-
-
-class ListRequests:
- implements(IListRequests)
-
- def __init__(self, mailing_list):
- self.mailing_list = mailing_list
-
- @property
- def count(self):
- return config.db.store.find(
- _Request, mailing_list=self.mailing_list).count()
-
- def count_of(self, request_type):
- return config.db.store.find(
- _Request,
- mailing_list=self.mailing_list, request_type=request_type).count()
-
- @property
- def held_requests(self):
- results = config.db.store.find(
- _Request, mailing_list=self.mailing_list)
- for request in results:
- yield request
-
- def of_type(self, request_type):
- results = config.db.store.find(
- _Request,
- mailing_list=self.mailing_list, request_type=request_type)
- for request in results:
- yield request
-
- def hold_request(self, request_type, key, data=None):
- if request_type not in RequestType:
- raise TypeError(request_type)
- if data is None:
- data_hash = None
- else:
- # We're abusing the pending database as a way of storing arbitrary
- # key/value pairs, where both are strings. This isn't ideal but
- # it lets us get auxiliary data almost for free. We may need to
- # lock this down more later.
- pendable = DataPendable()
- pendable.update(data)
- token = config.db.pendings.add(pendable, timedelta(days=5000))
- data_hash = token
- request = _Request(key, request_type, self.mailing_list, data_hash)
- config.db.store.add(request)
- return request.id
-
- def get_request(self, request_id):
- result = config.db.store.get(_Request, request_id)
- if result is None:
- return None
- if result.data_hash is None:
- return result.key, result.data_hash
- pendable = config.db.pendings.confirm(result.data_hash, expunge=False)
- data = dict()
- data.update(pendable)
- return result.key, data
-
- def delete_request(self, request_id):
- request = config.db.store.get(_Request, request_id)
- if request is None:
- raise KeyError(request_id)
- # Throw away the pended data.
- config.db.pendings.confirm(request.data_hash)
- config.db.store.remove(request)
-
-
-
-class Requests:
- implements(IRequests)
-
- def get_list_requests(self, mailing_list):
- return ListRequests(mailing_list)
-
-
-
-class _Request(Model):
- """Table for mailing list hold requests."""
-
- id = Int(primary=True, default=AutoReload)
- key = Unicode()
- request_type = Enum()
- data_hash = RawStr()
-
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
-
- def __init__(self, key, request_type, mailing_list, data_hash):
- self.key = key
- self.request_type = request_type
- self.mailing_list = mailing_list
- self.data_hash = data_hash
diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py
deleted file mode 100644
index 55723893d..000000000
--- a/Mailman/database/model/roster.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""An implementation of an IRoster.
-
-These are hard-coded rosters which know how to filter a set of members to find
-the ones that fit a particular role. These are used as the member, owner,
-moderator, and administrator roster filters.
-"""
-
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman.configuration import config
-from Mailman.constants import SystemDefaultPreferences
-from Mailman.database.model import Address, Member
-from Mailman.interfaces import DeliveryMode, IRoster, MemberRole
-
-
-
-class AbstractRoster(object):
- """An abstract IRoster class.
-
- This class takes the simple approach of implemented the 'users' and
- 'addresses' properties in terms of the 'members' property. This may not
- be the most efficient way, but it works.
-
- This requires that subclasses implement the 'members' property.
- """
- implements(IRoster)
-
- role = None
-
- def __init__(self, mlist):
- self._mlist = mlist
-
- @property
- def members(self):
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=self.role):
- yield member
-
- @property
- def users(self):
- # Members are linked to addresses, which in turn are linked to users.
- # So while the 'members' attribute does most of the work, we have to
- # keep a set of unique users. It's possible for the same user to be
- # subscribed to a mailing list multiple times with different
- # addresses.
- users = set(member.address.user for member in self.members)
- for user in users:
- yield user
-
- @property
- def addresses(self):
- # Every Member is linked to exactly one address so the 'members'
- # attribute does most of the work.
- for member in self.members:
- yield member.address
-
- def get_member(self, address):
- results = config.db.store.find(
- Member,
- Member.mailing_list == self._mlist.fqdn_listname,
- Member.role == self.role,
- Address.address == address,
- Member.address_id == Address.id)
- if results.count() == 0:
- return None
- elif results.count() == 1:
- return results[0]
- else:
- raise AssertionError('Too many matching member results: %s' %
- results.count())
-
-
-
-class MemberRoster(AbstractRoster):
- """Return all the members of a list."""
-
- name = 'member'
- role = MemberRole.member
-
-
-
-class OwnerRoster(AbstractRoster):
- """Return all the owners of a list."""
-
- name = 'owner'
- role = MemberRole.owner
-
-
-
-class ModeratorRoster(AbstractRoster):
- """Return all the owners of a list."""
-
- name = 'moderator'
- role = MemberRole.moderator
-
-
-
-class AdministratorRoster(AbstractRoster):
- """Return all the administrators of a list."""
-
- name = 'administrator'
-
- @property
- def members(self):
- # Administrators are defined as the union of the owners and the
- # moderators.
- members = config.db.store.find(
- Member,
- Member.mailing_list == self._mlist.fqdn_listname,
- Or(Member.role == MemberRole.owner,
- Member.role == MemberRole.moderator))
- for member in members:
- yield member
-
- def get_member(self, address):
- results = config.db.store.find(
- Member,
- Member.mailing_list == self._mlist.fqdn_listname,
- Or(Member.role == MemberRole.moderator,
- Member.role == MemberRole.owner),
- Address.address == address,
- Member.address_id == Address.id)
- if results.count() == 0:
- return None
- elif results.count() == 1:
- return results[0]
- else:
- raise AssertionError(
- 'Too many matching member results: %s' % results)
-
-
-
-class RegularMemberRoster(AbstractRoster):
- """Return all the regular delivery members of a list."""
-
- name = 'regular_members'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have a regular delivery mode.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode == DeliveryMode.regular:
- yield member
-
-
-
-_digest_modes = (
- DeliveryMode.mime_digests,
- DeliveryMode.plaintext_digests,
- DeliveryMode.summary_digests,
- )
-
-
-
-class DigestMemberRoster(AbstractRoster):
- """Return all the regular delivery members of a list."""
-
- name = 'regular_members'
-
- @property
- def members(self):
- # Query for all the Members which have a role of MemberRole.member and
- # are subscribed to this mailing list. Then return only those members
- # that have one of the digest delivery modes.
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname,
- role=MemberRole.member):
- if member.delivery_mode in _digest_modes:
- yield member
-
-
-
-class Subscribers(AbstractRoster):
- """Return all subscribed members regardless of their role."""
-
- name = 'subscribers'
-
- @property
- def members(self):
- for member in config.db.store.find(
- Member,
- mailing_list=self._mlist.fqdn_listname):
- yield member
diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py
deleted file mode 100644
index 84b5a4595..000000000
--- a/Mailman/database/model/user.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from email.utils import formataddr
-from storm.locals import *
-from zope.interface import implements
-
-from Mailman import Errors
-from Mailman.configuration import config
-from Mailman.database import Model
-from Mailman.database.model import Address
-from Mailman.database.model import Preferences
-from Mailman.interfaces import IUser
-
-
-
-class User(Model):
- implements(IUser)
-
- id = Int(primary=True)
- real_name = Unicode()
- password = Unicode()
-
- addresses = ReferenceSet(id, 'Address.user_id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
-
- def __repr__(self):
- return '' % (self.real_name, id(self))
-
- def link(self, address):
- if address.user is not None:
- raise Errors.AddressAlreadyLinkedError(address)
- address.user = self
-
- def unlink(self, address):
- if address.user is None:
- raise Errors.AddressNotLinkedError(address)
- address.user = None
-
- def controls(self, address):
- found = config.db.store.find(Address, address=address)
- if found.count() == 0:
- return False
- assert found.count() == 1, 'Unexpected count'
- return found[0].user is self
-
- def register(self, address, real_name=None):
- # First, see if the address already exists
- addrobj = config.db.store.find(Address, address=address).one()
- if addrobj is None:
- if real_name is None:
- real_name = u''
- addrobj = Address(address=address, real_name=real_name)
- addrobj.preferences = Preferences()
- # Link the address to the user if it is not already linked.
- if addrobj.user is not None:
- raise Errors.AddressAlreadyLinkedError(addrobj)
- addrobj.user = self
- return addrobj
diff --git a/Mailman/database/model/version.py b/Mailman/database/model/version.py
deleted file mode 100644
index 2f4ff3f4d..000000000
--- a/Mailman/database/model/version.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (C) 2007 by the Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-from storm.locals import *
-from Mailman.database import Model
-
-
-
-class Version(Model):
- id = Int(primary=True)
- component = Unicode()
- version = Int()
-
- def __init__(self, component, version):
- self.component = component
- self.version = version
diff --git a/Mailman/database/pending.py b/Mailman/database/pending.py
new file mode 100644
index 000000000..1a272391d
--- /dev/null
+++ b/Mailman/database/pending.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Implementations of the IPendable and IPending interfaces."""
+
+import sys
+import time
+import random
+import hashlib
+import datetime
+
+from storm.locals import *
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from Mailman.configuration import config
+from Mailman.database.model import Model
+from Mailman.interfaces import (
+ IPendable, IPended, IPendedKeyValue, IPendings)
+
+
+
+class PendedKeyValue(Model):
+ """A pended key/value pair, tied to a token."""
+
+ implements(IPendedKeyValue)
+
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+
+ id = Int(primary=True)
+ key = Unicode()
+ value = Unicode()
+ pended_id = Int()
+
+
+class Pended(Model):
+ """A pended event, tied to a token."""
+
+ implements(IPended)
+
+ def __init__(self, token, expiration_date):
+ self.token = token
+ self.expiration_date = expiration_date
+
+ id = Int(primary=True)
+ token = RawStr()
+ expiration_date = DateTime()
+ key_values = ReferenceSet(id, PendedKeyValue.pended_id)
+
+
+
+class UnpendedPendable(dict):
+ implements(IPendable)
+
+
+
+class Pendings(object):
+ """Implementation of the IPending interface."""
+
+ implements(IPendings)
+
+ def add(self, pendable, lifetime=None):
+ verifyObject(IPendable, pendable)
+ # Calculate the token and the lifetime.
+ if lifetime is None:
+ lifetime = config.PENDING_REQUEST_LIFE
+ # Calculate a unique token. Algorithm vetted by the Timbot. time()
+ # has high resolution on Linux, clock() on Windows. random gives us
+ # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and
+ # clock values basically help obscure the random number generator, as
+ # does the hash calculation. The integral parts of the time values
+ # are discarded because they're the most predictable bits.
+ for attempts in range(3):
+ now = time.time()
+ x = random.random() + now % 1.0 + time.clock() % 1.0
+ # Use sha1 because it produces shorter strings.
+ token = hashlib.sha1(repr(x)).hexdigest()
+ # In practice, we'll never get a duplicate, but we'll be anal
+ # about checking anyway.
+ if config.db.store.find(Pended, token=token).count() == 0:
+ break
+ else:
+ raise AssertionError('Could not find a valid pendings token')
+ # Create the record, and then the individual key/value pairs.
+ pending = Pended(
+ token=token,
+ expiration_date=datetime.datetime.now() + lifetime)
+ for key, value in pendable.items():
+ if isinstance(key, str):
+ key = unicode(key, 'utf-8')
+ if isinstance(value, str):
+ value = unicode(value, 'utf-8')
+ elif type(value) is int:
+ value = u'__builtin__.int\1%s' % value
+ elif type(value) is float:
+ value = u'__builtin__.float\1%s' % value
+ elif type(value) is bool:
+ value = u'__builtin__.bool\1%s' % value
+ keyval = PendedKeyValue(key=key, value=value)
+ pending.key_values.add(keyval)
+ config.db.store.add(pending)
+ return token
+
+ def confirm(self, token, expunge=True):
+ store = config.db.store
+ pendings = store.find(Pended, token=token)
+ if pendings.count() == 0:
+ return None
+ assert pendings.count() == 1, (
+ 'Unexpected token count: %d' % pendings.count())
+ pending = pendings[0]
+ pendable = UnpendedPendable()
+ # Find all PendedKeyValue entries that are associated with the pending
+ # object's ID. Watch out for type conversions.
+ for keyvalue in store.find(PendedKeyValue,
+ PendedKeyValue.pended_id == pending.id):
+ if keyvalue.value is not None and '\1' in keyvalue.value:
+ typename, value = keyvalue.value.split('\1', 1)
+ package, classname = typename.rsplit('.', 1)
+ __import__(package)
+ module = sys.modules[package]
+ pendable[keyvalue.key] = getattr(module, classname)(value)
+ else:
+ pendable[keyvalue.key] = keyvalue.value
+ if expunge:
+ store.remove(keyvalue)
+ if expunge:
+ store.remove(pending)
+ return pendable
+
+ def evict(self):
+ store = config.db.store
+ now = datetime.datetime.now()
+ for pending in store.find(Pended):
+ if pending.expiration_date < now:
+ # Find all PendedKeyValue entries that are associated with the
+ # pending object's ID.
+ q = store.find(PendedKeyValue,
+ PendedKeyValue.pended_id == pending.id)
+ for keyvalue in q:
+ store.remove(keyvalue)
+ store.remove(pending)
diff --git a/Mailman/database/preferences.py b/Mailman/database/preferences.py
new file mode 100644
index 000000000..726856f74
--- /dev/null
+++ b/Mailman/database/preferences.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.database.model import Model
+from Mailman.database.types import Enum
+from Mailman.interfaces import IPreferences
+
+
+
+class Preferences(Model):
+ implements(IPreferences)
+
+ id = Int(primary=True)
+ acknowledge_posts = Bool()
+ hide_address = Bool()
+ preferred_language = Unicode()
+ receive_list_copy = Bool()
+ receive_own_postings = Bool()
+ delivery_mode = Enum()
+ delivery_status = Enum()
+
+ def __repr__(self):
+ return '' % id(self)
diff --git a/Mailman/database/requests.py b/Mailman/database/requests.py
new file mode 100644
index 000000000..d86960f1d
--- /dev/null
+++ b/Mailman/database/requests.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Implementations of the IRequests and IListRequests interfaces."""
+
+from datetime import timedelta
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.database.model import Model
+from Mailman.database.types import Enum
+from Mailman.interfaces import IListRequests, IPendable, IRequests, RequestType
+
+
+__metaclass__ = type
+__all__ = [
+ 'Requests',
+ ]
+
+
+
+class DataPendable(dict):
+ implements(IPendable)
+
+
+
+class ListRequests:
+ implements(IListRequests)
+
+ def __init__(self, mailing_list):
+ self.mailing_list = mailing_list
+
+ @property
+ def count(self):
+ return config.db.store.find(
+ _Request, mailing_list=self.mailing_list).count()
+
+ def count_of(self, request_type):
+ return config.db.store.find(
+ _Request,
+ mailing_list=self.mailing_list, request_type=request_type).count()
+
+ @property
+ def held_requests(self):
+ results = config.db.store.find(
+ _Request, mailing_list=self.mailing_list)
+ for request in results:
+ yield request
+
+ def of_type(self, request_type):
+ results = config.db.store.find(
+ _Request,
+ mailing_list=self.mailing_list, request_type=request_type)
+ for request in results:
+ yield request
+
+ def hold_request(self, request_type, key, data=None):
+ if request_type not in RequestType:
+ raise TypeError(request_type)
+ if data is None:
+ data_hash = None
+ else:
+ # We're abusing the pending database as a way of storing arbitrary
+ # key/value pairs, where both are strings. This isn't ideal but
+ # it lets us get auxiliary data almost for free. We may need to
+ # lock this down more later.
+ pendable = DataPendable()
+ pendable.update(data)
+ token = config.db.pendings.add(pendable, timedelta(days=5000))
+ data_hash = token
+ request = _Request(key, request_type, self.mailing_list, data_hash)
+ config.db.store.add(request)
+ return request.id
+
+ def get_request(self, request_id):
+ result = config.db.store.get(_Request, request_id)
+ if result is None:
+ return None
+ if result.data_hash is None:
+ return result.key, result.data_hash
+ pendable = config.db.pendings.confirm(result.data_hash, expunge=False)
+ data = dict()
+ data.update(pendable)
+ return result.key, data
+
+ def delete_request(self, request_id):
+ request = config.db.store.get(_Request, request_id)
+ if request is None:
+ raise KeyError(request_id)
+ # Throw away the pended data.
+ config.db.pendings.confirm(request.data_hash)
+ config.db.store.remove(request)
+
+
+
+class Requests:
+ implements(IRequests)
+
+ def get_list_requests(self, mailing_list):
+ return ListRequests(mailing_list)
+
+
+
+class _Request(Model):
+ """Table for mailing list hold requests."""
+
+ id = Int(primary=True, default=AutoReload)
+ key = Unicode()
+ request_type = Enum()
+ data_hash = RawStr()
+
+ mailing_list_id = Int()
+ mailing_list = Reference(mailing_list_id, 'MailingList.id')
+
+ def __init__(self, key, request_type, mailing_list, data_hash):
+ self.key = key
+ self.request_type = request_type
+ self.mailing_list = mailing_list
+ self.data_hash = data_hash
diff --git a/Mailman/database/roster.py b/Mailman/database/roster.py
new file mode 100644
index 000000000..9bd97316f
--- /dev/null
+++ b/Mailman/database/roster.py
@@ -0,0 +1,210 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""An implementation of an IRoster.
+
+These are hard-coded rosters which know how to filter a set of members to find
+the ones that fit a particular role. These are used as the member, owner,
+moderator, and administrator roster filters.
+"""
+
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.constants import SystemDefaultPreferences
+from Mailman.database.address import Address
+from Mailman.database.member import Member
+from Mailman.interfaces import DeliveryMode, IRoster, MemberRole
+
+
+
+class AbstractRoster(object):
+ """An abstract IRoster class.
+
+ This class takes the simple approach of implemented the 'users' and
+ 'addresses' properties in terms of the 'members' property. This may not
+ be the most efficient way, but it works.
+
+ This requires that subclasses implement the 'members' property.
+ """
+ implements(IRoster)
+
+ role = None
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+
+ @property
+ def members(self):
+ for member in config.db.store.find(
+ Member,
+ mailing_list=self._mlist.fqdn_listname,
+ role=self.role):
+ yield member
+
+ @property
+ def users(self):
+ # Members are linked to addresses, which in turn are linked to users.
+ # So while the 'members' attribute does most of the work, we have to
+ # keep a set of unique users. It's possible for the same user to be
+ # subscribed to a mailing list multiple times with different
+ # addresses.
+ users = set(member.address.user for member in self.members)
+ for user in users:
+ yield user
+
+ @property
+ def addresses(self):
+ # Every Member is linked to exactly one address so the 'members'
+ # attribute does most of the work.
+ for member in self.members:
+ yield member.address
+
+ def get_member(self, address):
+ results = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Member.role == self.role,
+ Address.address == address,
+ Member.address_id == Address.id)
+ if results.count() == 0:
+ return None
+ elif results.count() == 1:
+ return results[0]
+ else:
+ raise AssertionError('Too many matching member results: %s' %
+ results.count())
+
+
+
+class MemberRoster(AbstractRoster):
+ """Return all the members of a list."""
+
+ name = 'member'
+ role = MemberRole.member
+
+
+
+class OwnerRoster(AbstractRoster):
+ """Return all the owners of a list."""
+
+ name = 'owner'
+ role = MemberRole.owner
+
+
+
+class ModeratorRoster(AbstractRoster):
+ """Return all the owners of a list."""
+
+ name = 'moderator'
+ role = MemberRole.moderator
+
+
+
+class AdministratorRoster(AbstractRoster):
+ """Return all the administrators of a list."""
+
+ name = 'administrator'
+
+ @property
+ def members(self):
+ # Administrators are defined as the union of the owners and the
+ # moderators.
+ members = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Or(Member.role == MemberRole.owner,
+ Member.role == MemberRole.moderator))
+ for member in members:
+ yield member
+
+ def get_member(self, address):
+ results = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Or(Member.role == MemberRole.moderator,
+ Member.role == MemberRole.owner),
+ Address.address == address,
+ Member.address_id == Address.id)
+ if results.count() == 0:
+ return None
+ elif results.count() == 1:
+ return results[0]
+ else:
+ raise AssertionError(
+ 'Too many matching member results: %s' % results)
+
+
+
+class RegularMemberRoster(AbstractRoster):
+ """Return all the regular delivery members of a list."""
+
+ name = 'regular_members'
+
+ @property
+ def members(self):
+ # Query for all the Members which have a role of MemberRole.member and
+ # are subscribed to this mailing list. Then return only those members
+ # that have a regular delivery mode.
+ for member in config.db.store.find(
+ Member,
+ mailing_list=self._mlist.fqdn_listname,
+ role=MemberRole.member):
+ if member.delivery_mode == DeliveryMode.regular:
+ yield member
+
+
+
+_digest_modes = (
+ DeliveryMode.mime_digests,
+ DeliveryMode.plaintext_digests,
+ DeliveryMode.summary_digests,
+ )
+
+
+
+class DigestMemberRoster(AbstractRoster):
+ """Return all the regular delivery members of a list."""
+
+ name = 'regular_members'
+
+ @property
+ def members(self):
+ # Query for all the Members which have a role of MemberRole.member and
+ # are subscribed to this mailing list. Then return only those members
+ # that have one of the digest delivery modes.
+ for member in config.db.store.find(
+ Member,
+ mailing_list=self._mlist.fqdn_listname,
+ role=MemberRole.member):
+ if member.delivery_mode in _digest_modes:
+ yield member
+
+
+
+class Subscribers(AbstractRoster):
+ """Return all subscribed members regardless of their role."""
+
+ name = 'subscribers'
+
+ @property
+ def members(self):
+ for member in config.db.store.find(
+ Member,
+ mailing_list=self._mlist.fqdn_listname):
+ yield member
diff --git a/Mailman/database/user.py b/Mailman/database/user.py
new file mode 100644
index 000000000..3ca320f36
--- /dev/null
+++ b/Mailman/database/user.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from email.utils import formataddr
+from storm.locals import *
+from zope.interface import implements
+
+from Mailman import Errors
+from Mailman.configuration import config
+from Mailman.database.model import Model
+from Mailman.database.address import Address
+from Mailman.database.preferences import Preferences
+from Mailman.interfaces import IUser
+
+
+
+class User(Model):
+ implements(IUser)
+
+ id = Int(primary=True)
+ real_name = Unicode()
+ password = Unicode()
+
+ addresses = ReferenceSet(id, 'Address.user_id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
+
+ def __repr__(self):
+ return '' % (self.real_name, id(self))
+
+ def link(self, address):
+ if address.user is not None:
+ raise Errors.AddressAlreadyLinkedError(address)
+ address.user = self
+
+ def unlink(self, address):
+ if address.user is None:
+ raise Errors.AddressNotLinkedError(address)
+ address.user = None
+
+ def controls(self, address):
+ found = config.db.store.find(Address, address=address)
+ if found.count() == 0:
+ return False
+ assert found.count() == 1, 'Unexpected count'
+ return found[0].user is self
+
+ def register(self, address, real_name=None):
+ # First, see if the address already exists
+ addrobj = config.db.store.find(Address, address=address).one()
+ if addrobj is None:
+ if real_name is None:
+ real_name = u''
+ addrobj = Address(address=address, real_name=real_name)
+ addrobj.preferences = Preferences()
+ # Link the address to the user if it is not already linked.
+ if addrobj.user is not None:
+ raise Errors.AddressAlreadyLinkedError(addrobj)
+ addrobj.user = self
+ return addrobj
diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py
index ab41409d8..19024d06c 100644
--- a/Mailman/database/usermanager.py
+++ b/Mailman/database/usermanager.py
@@ -25,6 +25,9 @@ from zope.interface import implements
from Mailman import Errors
from Mailman.configuration import config
+from Mailman.database.address import Address
+from Mailman.database.preferences import Preferences
+from Mailman.database.user import User
from Mailman.interfaces import IUserManager
@@ -33,8 +36,6 @@ class UserManager(object):
implements(IUserManager)
def create_user(self, address=None, real_name=None):
- # Avoid circular imports.
- from Mailman.database.model import Address, Preferences, User
user = User()
user.real_name = (u'' if real_name is None else real_name)
if address:
@@ -50,14 +51,10 @@ class UserManager(object):
@property
def users(self):
- # Avoid circular imports.
- from Mailman.database.model import User
for user in config.db.store.find(User):
yield user
def get_user(self, address):
- # Avoid circular imports.
- from Mailman.database.model import Address
addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 0:
return None
@@ -67,8 +64,6 @@ class UserManager(object):
raise AssertionError('Unexpected query count')
def create_address(self, address, real_name=None):
- # Avoid circular imports.
- from Mailman.database.model import Address, Preferences
addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 1:
found = addresses[0]
@@ -91,8 +86,6 @@ class UserManager(object):
config.db.store.remove(address)
def get_address(self, address):
- # Avoid circular imports.
- from Mailman.database.model import Address
addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 0:
return None
@@ -103,7 +96,5 @@ class UserManager(object):
@property
def addresses(self):
- # Avoid circular imports.
- from Mailman.database.model.address import Address
for address in config.db.store.find(Address):
yield address
diff --git a/Mailman/database/version.py b/Mailman/database/version.py
new file mode 100644
index 000000000..a9aaeb45d
--- /dev/null
+++ b/Mailman/database/version.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+from storm.locals import *
+from Mailman.database.model import Model
+
+
+
+class Version(Model):
+ id = Int(primary=True)
+ component = Unicode()
+ version = Int()
+
+ def __init__(self, component, version):
+ self.component = component
+ self.version = version
diff --git a/data/PythonPowered.png b/data/PythonPowered.png
new file mode 100644
index 000000000..2e9d99c2d
Binary files /dev/null and b/data/PythonPowered.png differ
diff --git a/data/__init__.py b/data/__init__.py
new file mode 100755
index 000000000..e69de29bb
diff --git a/data/coverage.py b/data/coverage.py
new file mode 100644
index 000000000..66e55e0c4
--- /dev/null
+++ b/data/coverage.py
@@ -0,0 +1,952 @@
+#!/usr/bin/python
+#
+# Perforce Defect Tracking Integration Project
+#
+#
+# COVERAGE.PY -- COVERAGE TESTING
+#
+# Gareth Rees, Ravenbrook Limited, 2001-12-04
+# Ned Batchelder, 2004-12-12
+# http://nedbatchelder.com/code/modules/coverage.html
+#
+#
+# 1. INTRODUCTION
+#
+# This module provides coverage testing for Python code.
+#
+# The intended readership is all Python developers.
+#
+# This document is not confidential.
+#
+# See [GDR 2001-12-04a] for the command-line interface, programmatic
+# interface and limitations. See [GDR 2001-12-04b] for requirements and
+# design.
+
+r"""Usage:
+
+coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...]
+ Execute module, passing the given command-line arguments, collecting
+ coverage data. With the -p option, write to a temporary file containing
+ the machine name and process ID.
+
+coverage.py -e
+ Erase collected coverage data.
+
+coverage.py -c
+ Collect data from multiple coverage files (as created by -p option above)
+ and store it into a single file representing the union of the coverage.
+
+coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
+ Report on the statement coverage for the given files. With the -m
+ option, show line numbers of the statements that weren't executed.
+
+coverage.py -a [-d dir] [-o dir1,dir2,...] FILE1 FILE2 ...
+ Make annotated copies of the given files, marking statements that
+ are executed with > and statements that are missed with !. With
+ the -d option, make the copies in that directory. Without the -d
+ option, make each copy in the same directory as the original.
+
+-o dir,dir2,...
+ Omit reporting or annotating files when their filename path starts with
+ a directory listed in the omit list.
+ e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits
+
+Coverage data is saved in the file .coverage by default. Set the
+COVERAGE_FILE environment variable to save it somewhere else."""
+
+__version__ = "2.6.20060823" # see detailed history at the end of this file.
+
+import compiler
+import compiler.visitor
+import os
+import re
+import string
+import sys
+import threading
+import types
+from socket import gethostname
+
+# 2. IMPLEMENTATION
+#
+# This uses the "singleton" pattern.
+#
+# The word "morf" means a module object (from which the source file can
+# be deduced by suitable manipulation of the __file__ attribute) or a
+# filename.
+#
+# When we generate a coverage report we have to canonicalize every
+# filename in the coverage dictionary just in case it refers to the
+# module we are reporting on. It seems a shame to throw away this
+# information so the data in the coverage dictionary is transferred to
+# the 'cexecuted' dictionary under the canonical filenames.
+#
+# The coverage dictionary is called "c" and the trace function "t". The
+# reason for these short names is that Python looks up variables by name
+# at runtime and so execution time depends on the length of variables!
+# In the bottleneck of this application it's appropriate to abbreviate
+# names to increase speed.
+
+class StatementFindingAstVisitor(compiler.visitor.ASTVisitor):
+ def __init__(self, statements, excluded, suite_spots):
+ compiler.visitor.ASTVisitor.__init__(self)
+ self.statements = statements
+ self.excluded = excluded
+ self.suite_spots = suite_spots
+ self.excluding_suite = 0
+
+ def doRecursive(self, node):
+ self.recordNodeLine(node)
+ for n in node.getChildNodes():
+ self.dispatch(n)
+
+ visitStmt = visitModule = doRecursive
+
+ def doCode(self, node):
+ if hasattr(node, 'decorators') and node.decorators:
+ self.dispatch(node.decorators)
+ self.recordAndDispatch(node.code)
+ else:
+ self.doSuite(node, node.code)
+
+ visitFunction = visitClass = doCode
+
+ def getFirstLine(self, node):
+ # Find the first line in the tree node.
+ lineno = node.lineno
+ for n in node.getChildNodes():
+ f = self.getFirstLine(n)
+ if lineno and f:
+ lineno = min(lineno, f)
+ else:
+ lineno = lineno or f
+ return lineno
+
+ def getLastLine(self, node):
+ # Find the first line in the tree node.
+ lineno = node.lineno
+ for n in node.getChildNodes():
+ lineno = max(lineno, self.getLastLine(n))
+ return lineno
+
+ def doStatement(self, node):
+ self.recordLine(self.getFirstLine(node))
+
+ visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \
+ visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
+ doStatement
+
+ def recordNodeLine(self, node):
+ return self.recordLine(node.lineno)
+
+ def recordLine(self, lineno):
+ # Returns a bool, whether the line is included or excluded.
+ if lineno:
+ # Multi-line tests introducing suites have to get charged to their
+ # keyword.
+ if lineno in self.suite_spots:
+ lineno = self.suite_spots[lineno][0]
+ # If we're inside an exluded suite, record that this line was
+ # excluded.
+ if self.excluding_suite:
+ self.excluded[lineno] = 1
+ return 0
+ # If this line is excluded, or suite_spots maps this line to
+ # another line that is exlcuded, then we're excluded.
+ elif self.excluded.has_key(lineno) or \
+ self.suite_spots.has_key(lineno) and \
+ self.excluded.has_key(self.suite_spots[lineno][1]):
+ return 0
+ # Otherwise, this is an executable line.
+ else:
+ self.statements[lineno] = 1
+ return 1
+ return 0
+
+ default = recordNodeLine
+
+ def recordAndDispatch(self, node):
+ self.recordNodeLine(node)
+ self.dispatch(node)
+
+ def doSuite(self, intro, body, exclude=0):
+ exsuite = self.excluding_suite
+ if exclude or (intro and not self.recordNodeLine(intro)):
+ self.excluding_suite = 1
+ self.recordAndDispatch(body)
+ self.excluding_suite = exsuite
+
+ def doPlainWordSuite(self, prevsuite, suite):
+ # Finding the exclude lines for else's is tricky, because they aren't
+ # present in the compiler parse tree. Look at the previous suite,
+ # and find its last line. If any line between there and the else's
+ # first line are excluded, then we exclude the else.
+ lastprev = self.getLastLine(prevsuite)
+ firstelse = self.getFirstLine(suite)
+ for l in range(lastprev+1, firstelse):
+ if self.suite_spots.has_key(l):
+ self.doSuite(None, suite, exclude=self.excluded.has_key(l))
+ break
+ else:
+ self.doSuite(None, suite)
+
+ def doElse(self, prevsuite, node):
+ if node.else_:
+ self.doPlainWordSuite(prevsuite, node.else_)
+
+ def visitFor(self, node):
+ self.doSuite(node, node.body)
+ self.doElse(node.body, node)
+
+ def visitIf(self, node):
+ # The first test has to be handled separately from the rest.
+ # The first test is credited to the line with the "if", but the others
+ # are credited to the line with the test for the elif.
+ self.doSuite(node, node.tests[0][1])
+ for t, n in node.tests[1:]:
+ self.doSuite(t, n)
+ self.doElse(node.tests[-1][1], node)
+
+ def visitWhile(self, node):
+ self.doSuite(node, node.body)
+ self.doElse(node.body, node)
+
+ def visitTryExcept(self, node):
+ self.doSuite(node, node.body)
+ for i in range(len(node.handlers)):
+ a, b, h = node.handlers[i]
+ if not a:
+ # It's a plain "except:". Find the previous suite.
+ if i > 0:
+ prev = node.handlers[i-1][2]
+ else:
+ prev = node.body
+ self.doPlainWordSuite(prev, h)
+ else:
+ self.doSuite(a, h)
+ self.doElse(node.handlers[-1][2], node)
+
+ def visitTryFinally(self, node):
+ self.doSuite(node, node.body)
+ self.doPlainWordSuite(node.body, node.final)
+
+ def visitGlobal(self, node):
+ # "global" statements don't execute like others (they don't call the
+ # trace function), so don't record their line numbers.
+ pass
+
+the_coverage = None
+
+class CoverageException(Exception): pass
+
+class coverage:
+ # Name of the cache file (unless environment variable is set).
+ cache_default = ".coverage"
+
+ # Environment variable naming the cache file.
+ cache_env = "COVERAGE_FILE"
+
+ # A dictionary with an entry for (Python source file name, line number
+ # in that file) if that line has been executed.
+ c = {}
+
+ # A map from canonical Python source file name to a dictionary in
+ # which there's an entry for each line number that has been
+ # executed.
+ cexecuted = {}
+
+ # Cache of results of calling the analysis2() method, so that you can
+ # specify both -r and -a without doing double work.
+ analysis_cache = {}
+
+ # Cache of results of calling the canonical_filename() method, to
+ # avoid duplicating work.
+ canonical_filename_cache = {}
+
+ def __init__(self):
+ global the_coverage
+ if the_coverage:
+ raise CoverageException, "Only one coverage object allowed."
+ self.usecache = 1
+ self.cache = None
+ self.exclude_re = ''
+ self.nesting = 0
+ self.cstack = []
+ self.xstack = []
+ self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep)
+
+ # t(f, x, y). This method is passed to sys.settrace as a trace function.
+ # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and
+ # the arguments and return value of the trace function.
+ # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code
+ # objects.
+
+ def t(self, f, w, a): #pragma: no cover
+ if w == 'line':
+ self.c[(f.f_code.co_filename, f.f_lineno)] = 1
+ for c in self.cstack:
+ c[(f.f_code.co_filename, f.f_lineno)] = 1
+ return self.t
+
+ def help(self, error=None):
+ if error:
+ print error
+ print
+ print __doc__
+ sys.exit(1)
+
+ def command_line(self, argv, help=None):
+ import getopt
+ help = help or self.help
+ settings = {}
+ optmap = {
+ '-a': 'annotate',
+ '-c': 'collect',
+ '-d:': 'directory=',
+ '-e': 'erase',
+ '-h': 'help',
+ '-i': 'ignore-errors',
+ '-m': 'show-missing',
+ '-p': 'parallel-mode',
+ '-r': 'report',
+ '-x': 'execute',
+ '-o:': 'omit=',
+ }
+ short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
+ long_opts = optmap.values()
+ options, args = getopt.getopt(argv, short_opts, long_opts)
+ for o, a in options:
+ if optmap.has_key(o):
+ settings[optmap[o]] = 1
+ elif optmap.has_key(o + ':'):
+ settings[optmap[o + ':']] = a
+ elif o[2:] in long_opts:
+ settings[o[2:]] = 1
+ elif o[2:] + '=' in long_opts:
+ settings[o[2:]+'='] = a
+ else: #pragma: no cover
+ pass # Can't get here, because getopt won't return anything unknown.
+
+ if settings.get('help'):
+ help()
+
+ for i in ['erase', 'execute']:
+ for j in ['annotate', 'report', 'collect']:
+ if settings.get(i) and settings.get(j):
+ help("You can't specify the '%s' and '%s' "
+ "options at the same time." % (i, j))
+
+ args_needed = (settings.get('execute')
+ or settings.get('annotate')
+ or settings.get('report'))
+ action = (settings.get('erase')
+ or settings.get('collect')
+ or args_needed)
+ if not action:
+ help("You must specify at least one of -e, -x, -c, -r, or -a.")
+ if not args_needed and args:
+ help("Unexpected arguments: %s" % " ".join(args))
+
+ self.get_ready(settings.get('parallel-mode'))
+ self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
+
+ if settings.get('erase'):
+ self.erase()
+ if settings.get('execute'):
+ if not args:
+ help("Nothing to do.")
+ sys.argv = args
+ self.start()
+ import __main__
+ sys.path[0] = os.path.dirname(sys.argv[0])
+ execfile(sys.argv[0], __main__.__dict__)
+ if settings.get('collect'):
+ self.collect()
+ if not args:
+ args = self.cexecuted.keys()
+
+ ignore_errors = settings.get('ignore-errors')
+ show_missing = settings.get('show-missing')
+ directory = settings.get('directory=')
+
+ omit = settings.get('omit=')
+ if omit is not None:
+ omit = omit.split(',')
+ else:
+ omit = []
+
+ if settings.get('report'):
+ self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
+ if settings.get('annotate'):
+ self.annotate(args, directory, ignore_errors, omit_prefixes=omit)
+
+ def use_cache(self, usecache, cache_file=None):
+ self.usecache = usecache
+ if cache_file and not self.cache:
+ self.cache_default = cache_file
+
+ def get_ready(self, parallel_mode=False):
+ if self.usecache and not self.cache:
+ self.cache = os.environ.get(self.cache_env, self.cache_default)
+ if parallel_mode:
+ self.cache += "." + gethostname() + "." + str(os.getpid())
+ self.restore()
+ self.analysis_cache = {}
+
+ def start(self, parallel_mode=False):
+ self.get_ready(parallel_mode)
+ if self.nesting == 0: #pragma: no cover
+ sys.settrace(self.t)
+ if hasattr(threading, 'settrace'):
+ threading.settrace(self.t)
+ self.nesting += 1
+
+ def stop(self):
+ self.nesting -= 1
+ if self.nesting == 0: #pragma: no cover
+ sys.settrace(None)
+ if hasattr(threading, 'settrace'):
+ threading.settrace(None)
+
+ def erase(self):
+ self.c = {}
+ self.analysis_cache = {}
+ self.cexecuted = {}
+ if self.cache and os.path.exists(self.cache):
+ os.remove(self.cache)
+ self.exclude_re = ""
+
+ def exclude(self, re):
+ if self.exclude_re:
+ self.exclude_re += "|"
+ self.exclude_re += "(" + re + ")"
+
+ def begin_recursive(self):
+ self.cstack.append(self.c)
+ self.xstack.append(self.exclude_re)
+
+ def end_recursive(self):
+ self.c = self.cstack.pop()
+ self.exclude_re = self.xstack.pop()
+
+ # save(). Save coverage data to the coverage cache.
+
+ def save(self):
+ if self.usecache and self.cache:
+ self.canonicalize_filenames()
+ cache = open(self.cache, 'wb')
+ import marshal
+ marshal.dump(self.cexecuted, cache)
+ cache.close()
+
+ # restore(). Restore coverage data from the coverage cache (if it exists).
+
+ def restore(self):
+ self.c = {}
+ self.cexecuted = {}
+ assert self.usecache
+ if os.path.exists(self.cache):
+ self.cexecuted = self.restore_file(self.cache)
+
+ def restore_file(self, file_name):
+ try:
+ cache = open(file_name, 'rb')
+ import marshal
+ cexecuted = marshal.load(cache)
+ cache.close()
+ if isinstance(cexecuted, types.DictType):
+ return cexecuted
+ else:
+ return {}
+ except:
+ return {}
+
+ # collect(). Collect data in multiple files produced by parallel mode
+
+ def collect(self):
+ cache_dir, local = os.path.split(self.cache)
+ for file in os.listdir(cache_dir):
+ if not file.startswith(local):
+ continue
+
+ full_path = os.path.join(cache_dir, file)
+ cexecuted = self.restore_file(full_path)
+ self.merge_data(cexecuted)
+
+ def merge_data(self, new_data):
+ for file_name, file_data in new_data.items():
+ if self.cexecuted.has_key(file_name):
+ self.merge_file_data(self.cexecuted[file_name], file_data)
+ else:
+ self.cexecuted[file_name] = file_data
+
+ def merge_file_data(self, cache_data, new_data):
+ for line_number in new_data.keys():
+ if not cache_data.has_key(line_number):
+ cache_data[line_number] = new_data[line_number]
+
+ # canonical_filename(filename). Return a canonical filename for the
+ # file (that is, an absolute path with no redundant components and
+ # normalized case). See [GDR 2001-12-04b, 3.3].
+
+ def canonical_filename(self, filename):
+ if not self.canonical_filename_cache.has_key(filename):
+ f = filename
+ if os.path.isabs(f) and not os.path.exists(f):
+ f = os.path.basename(f)
+ if not os.path.isabs(f):
+ for path in [os.curdir] + sys.path:
+ g = os.path.join(path, f)
+ if os.path.exists(g):
+ f = g
+ break
+ cf = os.path.normcase(os.path.abspath(f))
+ self.canonical_filename_cache[filename] = cf
+ return self.canonical_filename_cache[filename]
+
+ # canonicalize_filenames(). Copy results from "c" to "cexecuted",
+ # canonicalizing filenames on the way. Clear the "c" map.
+
+ def canonicalize_filenames(self):
+ for filename, lineno in self.c.keys():
+ f = self.canonical_filename(filename)
+ if not self.cexecuted.has_key(f):
+ self.cexecuted[f] = {}
+ self.cexecuted[f][lineno] = 1
+ self.c = {}
+
+ # morf_filename(morf). Return the filename for a module or file.
+
+ def morf_filename(self, morf):
+ if isinstance(morf, types.ModuleType):
+ if not hasattr(morf, '__file__'):
+ raise CoverageException, "Module has no __file__ attribute."
+ file = morf.__file__
+ else:
+ file = morf
+ return self.canonical_filename(file)
+
+ # analyze_morf(morf). Analyze the module or filename passed as
+ # the argument. If the source code can't be found, raise an error.
+ # Otherwise, return a tuple of (1) the canonical filename of the
+ # source code for the module, (2) a list of lines of statements
+ # in the source code, and (3) a list of lines of excluded statements.
+
+ def analyze_morf(self, morf):
+ if self.analysis_cache.has_key(morf):
+ return self.analysis_cache[morf]
+ filename = self.morf_filename(morf)
+ ext = os.path.splitext(filename)[1]
+ if ext == '.pyc':
+ if not os.path.exists(filename[0:-1]):
+ raise CoverageException, ("No source for compiled code '%s'."
+ % filename)
+ filename = filename[0:-1]
+ elif ext != '.py':
+ raise CoverageException, "File '%s' not Python source." % filename
+ source = open(filename, 'r')
+ lines, excluded_lines = self.find_executable_statements(
+ source.read(), exclude=self.exclude_re
+ )
+ source.close()
+ result = filename, lines, excluded_lines
+ self.analysis_cache[morf] = result
+ return result
+
+ def get_suite_spots(self, tree, spots):
+ import symbol, token
+ for i in range(1, len(tree)):
+ if type(tree[i]) == type(()):
+ if tree[i][0] == symbol.suite:
+ # Found a suite, look back for the colon and keyword.
+ lineno_colon = lineno_word = None
+ for j in range(i-1, 0, -1):
+ if tree[j][0] == token.COLON:
+ lineno_colon = tree[j][2]
+ elif tree[j][0] == token.NAME:
+ if tree[j][1] == 'elif':
+ # Find the line number of the first non-terminal
+ # after the keyword.
+ t = tree[j+1]
+ while t and token.ISNONTERMINAL(t[0]):
+ t = t[1]
+ if t:
+ lineno_word = t[2]
+ else:
+ lineno_word = tree[j][2]
+ break
+ elif tree[j][0] == symbol.except_clause:
+ # "except" clauses look like:
+ # ('except_clause', ('NAME', 'except', lineno), ...)
+ if tree[j][1][0] == token.NAME:
+ lineno_word = tree[j][1][2]
+ break
+ if lineno_colon and lineno_word:
+ # Found colon and keyword, mark all the lines
+ # between the two with the two line numbers.
+ for l in range(lineno_word, lineno_colon+1):
+ spots[l] = (lineno_word, lineno_colon)
+ self.get_suite_spots(tree[i], spots)
+
+ def find_executable_statements(self, text, exclude=None):
+ # Find lines which match an exclusion pattern.
+ excluded = {}
+ suite_spots = {}
+ if exclude:
+ reExclude = re.compile(exclude)
+ lines = text.split('\n')
+ for i in range(len(lines)):
+ if reExclude.search(lines[i]):
+ excluded[i+1] = 1
+
+ import parser
+ tree = parser.suite(text+'\n\n').totuple(1)
+ self.get_suite_spots(tree, suite_spots)
+
+ # Use the compiler module to parse the text and find the executable
+ # statements. We add newlines to be impervious to final partial lines.
+ statements = {}
+ ast = compiler.parse(text+'\n\n')
+ visitor = StatementFindingAstVisitor(statements, excluded, suite_spots)
+ compiler.walk(ast, visitor, walker=visitor)
+
+ lines = statements.keys()
+ lines.sort()
+ excluded_lines = excluded.keys()
+ excluded_lines.sort()
+ return lines, excluded_lines
+
+ # format_lines(statements, lines). Format a list of line numbers
+ # for printing by coalescing groups of lines as long as the lines
+ # represent consecutive statements. This will coalesce even if
+ # there are gaps between statements, so if statements =
+ # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then
+ # format_lines will return "1-2, 5-11, 13-14".
+
+ def format_lines(self, statements, lines):
+ pairs = []
+ i = 0
+ j = 0
+ start = None
+ pairs = []
+ while i < len(statements) and j < len(lines):
+ if statements[i] == lines[j]:
+ if start == None:
+ start = lines[j]
+ end = lines[j]
+ j = j + 1
+ elif start:
+ pairs.append((start, end))
+ start = None
+ i = i + 1
+ if start:
+ pairs.append((start, end))
+ def stringify(pair):
+ start, end = pair
+ if start == end:
+ return "%d" % start
+ else:
+ return "%d-%d" % (start, end)
+ return string.join(map(stringify, pairs), ", ")
+
+ # Backward compatibility with version 1.
+ def analysis(self, morf):
+ f, s, _, m, mf = self.analysis2(morf)
+ return f, s, m, mf
+
+ def analysis2(self, morf):
+ filename, statements, excluded = self.analyze_morf(morf)
+ self.canonicalize_filenames()
+ if not self.cexecuted.has_key(filename):
+ self.cexecuted[filename] = {}
+ missing = []
+ for line in statements:
+ if not self.cexecuted[filename].has_key(line):
+ missing.append(line)
+ return (filename, statements, excluded, missing,
+ self.format_lines(statements, missing))
+
+ def relative_filename(self, filename):
+ """ Convert filename to relative filename from self.relative_dir.
+ """
+ return filename.replace(self.relative_dir, "")
+
+ def morf_name(self, morf):
+ """ Return the name of morf as used in report.
+ """
+ if isinstance(morf, types.ModuleType):
+ return morf.__name__
+ else:
+ return self.relative_filename(os.path.splitext(morf)[0])
+
+ def filter_by_prefix(self, morfs, omit_prefixes):
+ """ Return list of morfs where the morf name does not begin
+ with any one of the omit_prefixes.
+ """
+ filtered_morfs = []
+ for morf in morfs:
+ for prefix in omit_prefixes:
+ if self.morf_name(morf).startswith(prefix):
+ break
+ else:
+ filtered_morfs.append(morf)
+
+ return filtered_morfs
+
+ def morf_name_compare(self, x, y):
+ return cmp(self.morf_name(x), self.morf_name(y))
+
+ def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]):
+ if not isinstance(morfs, types.ListType):
+ morfs = [morfs]
+ morfs = self.filter_by_prefix(morfs, omit_prefixes)
+ morfs.sort(self.morf_name_compare)
+
+ max_name = max([5,] + map(len, map(self.morf_name, morfs)))
+ fmt_name = "%%- %ds " % max_name
+ fmt_err = fmt_name + "%s: %s"
+ header = fmt_name % "Name" + " Stmts Exec Cover"
+ fmt_coverage = fmt_name + "% 6d % 6d % 5d%%"
+ if show_missing:
+ header = header + " Missing"
+ fmt_coverage = fmt_coverage + " %s"
+ if not file:
+ file = sys.stdout
+ print >>file, header
+ print >>file, "-" * len(header)
+ total_statements = 0
+ total_executed = 0
+ for morf in morfs:
+ name = self.morf_name(morf)
+ try:
+ _, statements, _, missing, readable = self.analysis2(morf)
+ n = len(statements)
+ m = n - len(missing)
+ if n > 0:
+ pc = 100.0 * m / n
+ else:
+ pc = 100.0
+ args = (name, n, m, pc)
+ if show_missing:
+ args = args + (readable,)
+ print >>file, fmt_coverage % args
+ total_statements = total_statements + n
+ total_executed = total_executed + m
+ except KeyboardInterrupt: #pragma: no cover
+ raise
+ except:
+ if not ignore_errors:
+ type, msg = sys.exc_info()[0:2]
+ print >>file, fmt_err % (name, type, msg)
+ if len(morfs) > 1:
+ print >>file, "-" * len(header)
+ if total_statements > 0:
+ pc = 100.0 * total_executed / total_statements
+ else:
+ pc = 100.0
+ args = ("TOTAL", total_statements, total_executed, pc)
+ if show_missing:
+ args = args + ("",)
+ print >>file, fmt_coverage % args
+
+ # annotate(morfs, ignore_errors).
+
+ blank_re = re.compile(r"\s*(#|$)")
+ else_re = re.compile(r"\s*else\s*:\s*(#|$)")
+
+ def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]):
+ morfs = self.filter_by_prefix(morfs, omit_prefixes)
+ for morf in morfs:
+ try:
+ filename, statements, excluded, missing, _ = self.analysis2(morf)
+ self.annotate_file(filename, statements, excluded, missing, directory)
+ except KeyboardInterrupt:
+ raise
+ except:
+ if not ignore_errors:
+ raise
+
+ def annotate_file(self, filename, statements, excluded, missing, directory=None):
+ source = open(filename, 'r')
+ if directory:
+ dest_file = os.path.join(directory,
+ os.path.basename(filename)
+ + ',cover')
+ else:
+ dest_file = filename + ',cover'
+ dest = open(dest_file, 'w')
+ lineno = 0
+ i = 0
+ j = 0
+ covered = 1
+ while 1:
+ line = source.readline()
+ if line == '':
+ break
+ lineno = lineno + 1
+ while i < len(statements) and statements[i] < lineno:
+ i = i + 1
+ while j < len(missing) and missing[j] < lineno:
+ j = j + 1
+ if i < len(statements) and statements[i] == lineno:
+ covered = j >= len(missing) or missing[j] > lineno
+ if self.blank_re.match(line):
+ dest.write(' ')
+ elif self.else_re.match(line):
+ # Special logic for lines containing only 'else:'.
+ # See [GDR 2001-12-04b, 3.2].
+ if i >= len(statements) and j >= len(missing):
+ dest.write('! ')
+ elif i >= len(statements) or j >= len(missing):
+ dest.write('> ')
+ elif statements[i] == missing[j]:
+ dest.write('! ')
+ else:
+ dest.write('> ')
+ elif lineno in excluded:
+ dest.write('- ')
+ elif covered:
+ dest.write('> ')
+ else:
+ dest.write('! ')
+ dest.write(line)
+ source.close()
+ dest.close()
+
+# Singleton object.
+the_coverage = coverage()
+
+# Module functions call methods in the singleton object.
+def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw)
+def start(*args, **kw): return the_coverage.start(*args, **kw)
+def stop(*args, **kw): return the_coverage.stop(*args, **kw)
+def erase(*args, **kw): return the_coverage.erase(*args, **kw)
+def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw)
+def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw)
+def exclude(*args, **kw): return the_coverage.exclude(*args, **kw)
+def analysis(*args, **kw): return the_coverage.analysis(*args, **kw)
+def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw)
+def report(*args, **kw): return the_coverage.report(*args, **kw)
+def annotate(*args, **kw): return the_coverage.annotate(*args, **kw)
+def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw)
+
+# Save coverage data when Python exits. (The atexit module wasn't
+# introduced until Python 2.0, so use sys.exitfunc when it's not
+# available.)
+try:
+ import atexit
+ atexit.register(the_coverage.save)
+except ImportError:
+ sys.exitfunc = the_coverage.save
+
+# Command-line interface.
+if __name__ == '__main__':
+ the_coverage.command_line(sys.argv[1:])
+
+
+# A. REFERENCES
+#
+# [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees;
+# Ravenbrook Limited; 2001-12-04;
+# .
+#
+# [GDR 2001-12-04b] "Statement coverage for Python: design and
+# analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04;
+# .
+#
+# [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)";
+# Guide van Rossum; 2001-07-20;
+# .
+#
+# [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum;
+# 2001-07-20; .
+#
+#
+# B. DOCUMENT HISTORY
+#
+# 2001-12-04 GDR Created.
+#
+# 2001-12-06 GDR Added command-line interface and source code
+# annotation.
+#
+# 2001-12-09 GDR Moved design and interface to separate documents.
+#
+# 2001-12-10 GDR Open cache file as binary on Windows. Allow
+# simultaneous -e and -x, or -a and -r.
+#
+# 2001-12-12 GDR Added command-line help. Cache analysis so that it
+# only needs to be done once when you specify -a and -r.
+#
+# 2001-12-13 GDR Improved speed while recording. Portable between
+# Python 1.5.2 and 2.1.1.
+#
+# 2002-01-03 GDR Module-level functions work correctly.
+#
+# 2002-01-07 GDR Update sys.path when running a file with the -x option,
+# so that it matches the value the program would get if it were run on
+# its own.
+#
+# 2004-12-12 NMB Significant code changes.
+# - Finding executable statements has been rewritten so that docstrings and
+# other quirks of Python execution aren't mistakenly identified as missing
+# lines.
+# - Lines can be excluded from consideration, even entire suites of lines.
+# - The filesystem cache of covered lines can be disabled programmatically.
+# - Modernized the code.
+#
+# 2004-12-14 NMB Minor tweaks. Return 'analysis' to its original behavior
+# and add 'analysis2'. Add a global for 'annotate', and factor it, adding
+# 'annotate_file'.
+#
+# 2004-12-31 NMB Allow for keyword arguments in the module global functions.
+# Thanks, Allen.
+#
+# 2005-12-02 NMB Call threading.settrace so that all threads are measured.
+# Thanks Martin Fuzzey. Add a file argument to report so that reports can be
+# captured to a different destination.
+#
+# 2005-12-03 NMB coverage.py can now measure itself.
+#
+# 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames,
+# and sorting and omitting files to report on.
+#
+# 2006-07-23 NMB Applied Joseph Tate's patch for function decorators.
+#
+# 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument
+# handling.
+#
+# 2006-08-22 NMB Applied Geoff Bache's parallel mode patch.
+#
+# 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line
+# logic for parallel mode and collect.
+
+# C. COPYRIGHT AND LICENCE
+#
+# Copyright 2001 Gareth Rees. All rights reserved.
+# Copyright 2004-2006 Ned Batchelder. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+# $Id: coverage.py 47 2006-08-24 01:08:48Z Ned $
diff --git a/data/gnu-head-tiny.jpg b/data/gnu-head-tiny.jpg
new file mode 100644
index 000000000..441be50d6
Binary files /dev/null and b/data/gnu-head-tiny.jpg differ
diff --git a/data/mailman-large.jpg b/data/mailman-large.jpg
new file mode 100644
index 000000000..e184f3c6e
Binary files /dev/null and b/data/mailman-large.jpg differ
diff --git a/data/mailman.cfg.in b/data/mailman.cfg.in
new file mode 100644
index 000000000..3e24acc46
--- /dev/null
+++ b/data/mailman.cfg.in
@@ -0,0 +1,45 @@
+# -*- python -*-
+
+# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""This module contains your site-specific settings.
+
+Use this to override the default settings in `Mailman/Defaults.py`. You only
+need to include those settings that you want to change, and unlike the old
+`mm_cfg.py` file, you do /not/ need to `import Defaults`. Its variables will
+automatically be available in this module's namespace.
+
+You should consult `Defaults.py` though for a complete listing of
+configuration variables that you can change.
+
+Mailman's installation procedure will never overwrite `mailman.cfg`.
+"""
+
+# This is the top-level run-time data directory. All other runtime data by
+# default lives under this directory.
+VAR_DIR = '$var_dir'
+
+# User name and id that owns the Mailman process and files.
+MAILMAN_UID = '$user_id'
+MAILMAN_USER = '$user_name'
+
+# Group name and id that owns the Mailman process and files.
+MAILMAN_GID = '$group_id'
+MAILMAN_GROUP = '$group_name'
+
+LANGUAGES = '$languages'
diff --git a/data/mailman.in b/data/mailman.in
new file mode 100644
index 000000000..4fb0612f3
--- /dev/null
+++ b/data/mailman.in
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# mailman This shell script starts and stops GNU Mailman.
+#
+# Copyright (C) 2001-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Copy this file to /etc/init.d/ (or /etc/rc.d/init.d/ depending on
+# your system) and activate it as such:
+#
+# On Debian, type "update-rc.d mailman defaults"
+# On RedHat, and derivatives, install with "chkconfig --add mailman"
+#
+# chkconfig: 2345 98 12
+# description: Mailman is the GNU Mailing List Manager, a program that \
+# manages electronic mail discussion groups. For more \
+# on GNU Mailman see http://www.list.org
+# processname: mailmanctl
+# config: @prefix@/Mailman/mm_cfg.py
+# pidfile: @prefix@/data/master-qrunner.pid
+
+PYTHON=@PYTHON@
+MAILMANHOME=@prefix@
+MAILMANCTL=$MAILMANHOME/bin/mailmanctl
+
+case "$1" in
+'start')
+ #rm -f $MAILMANHOME/locks/*
+ $PYTHON $MAILMANCTL -s -q start
+ ;;
+
+'stop')
+ $PYTHON $MAILMANCTL -q stop
+ ;;
+
+'restart')
+ $PYTHON $MAILMANCTL -q restart
+ ;;
+
+esac
+exit 0
diff --git a/data/mailman.jpg b/data/mailman.jpg
new file mode 100644
index 000000000..94a4c0112
Binary files /dev/null and b/data/mailman.jpg differ
diff --git a/data/mm-icon.png b/data/mm-icon.png
new file mode 100644
index 000000000..42c3cf759
Binary files /dev/null and b/data/mm-icon.png differ
diff --git a/data/paths.py.in b/data/paths.py.in
new file mode 100644
index 000000000..a840e3f14
--- /dev/null
+++ b/data/paths.py.in
@@ -0,0 +1,90 @@
+# -*- python -*-
+
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+# configure turns this file into paths.py which is installed in the bin
+# directory. By importing this module, sys.path gets `hacked' so that the
+# $prefix/Mailman and $prefix/pythonlib directories are inserted at the start
+# of that list. This file exports two attributes that other modules may use
+# to get the absolute path to the installed Mailman distribution.
+#
+# Note that we can't use site.addsitedir() because that ends up appending
+# directories to sys.path and we really need to add them to the front so that
+# they override anything in the system Python.
+
+import os
+import sys
+
+# Some scripts expect this attribute to be in this module
+prefix = '@prefix@'
+exec_prefix = '@exec_prefix@'
+
+# Work around a bogus autoconf 2.12 bug
+if exec_prefix == '${prefix}':
+ exec_prefix = prefix
+
+pythonlib = os.path.join(prefix, 'pythonlib', 'lib', 'python')
+
+# Hack the path to include the parent directory of $prefix/Mailman
+sys.path.insert(0, prefix)
+
+# Much of this is ripped off from site.py
+paths = set()
+for dirname in sys.path:
+ try:
+ if os.path.isdir(dirname):
+ paths.add(os.path.normcase(os.path.abspath(dirname)))
+ except TypeError:
+ pass
+
+extra_paths = [pythonlib]
+for name in sorted(os.listdir(pythonlib)):
+ if os.path.splitext(name)[1] == '.pth':
+ filename = os.path.join(pythonlib, name)
+ try:
+ fp = open(filename, 'rU')
+ except IOError:
+ continue
+ try:
+ for line in fp:
+ if line.startswith('#'):
+ continue
+ if line.startswith('import'):
+ exec line
+ continue
+ line = line.rstrip()
+ path = os.path.abspath(os.path.join(pythonlib, line))
+ path = os.path.normcase(path)
+ if not path in paths and os.path.exists(path):
+ # Here's what's different than site.py!
+ extra_paths.append(path)
+ paths.add(path)
+ finally:
+ fp.close()
+# Add the new paths to the front of sys.path
+sys.path[0:0] = extra_paths
+
+
+# Arabic and Hebrew (RFC-1556) encoding aliases. (temporary solution)
+import encodings.aliases
+encodings.aliases.aliases.update({
+ 'iso_8859_6_e': 'iso8859_6',
+ 'iso_8859_6_i': 'iso8859_6',
+ 'iso_8859_8_e': 'iso8859_8',
+ 'iso_8859_8_i': 'iso8859_8',
+ })
--
cgit v1.3.1