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