#!/usr/bin/env python # -*- coding: utf-8 -*- import os os.environ['DJANGO_SETTINGS_MODULE']='web.settings' from web.library.models import * from web.library.fileformat import read_actionlist, write_actionlist from django.db.models import Q from django.db import transaction from util import * import getopt import sys import types import traceback import search import placement import tempfile file_encoding = 'utf8' def show_book_or_person(ids, commit_format=False, tmp_file=False): objects = map(get_book_or_person, ids) for i in range(len(ids)): if not objects[i]: objects[i] = 'No book or person with id %s.\n' % ids[i] elif commit_format: objects[i] = objects[i].to_dict() else: objects[i] = objects[i].to_string() if commit_format: output = write_actionlist(objects) else: output = '\n'.join(objects) if tmp_file: filename = write_tmpfile('.'.join(ids), output) print filename return filename else: print output.strip() def get_book_or_person(id): books = Book.objects.filter(Q(isbn=id)|Q(id__id=id)).all() persons = Person.objects.filter(id=id) if len(books) + len(persons) > 1: print 'Warning: More than one match for id %d.' % id print 'This should not happen.' if len(books) > 0: return books[0] if len(persons) > 0: return persons[0] def remove_duplicates(list): d = {} for i in list: d[i]=None return d.keys() def search_person_cmd(search_strings, search_description=False): people = search_person(search_strings, search_description) format = '%-20s %-10s %-70s' for person in people: name = cut_str(person.first_name+' '+person.last_name, 20) p_id = cut_str(person.id, 10) books = cut_str(', '.join(map(lambda x: x.book.title, person.books.all())), 70) print format % (name,p_id,books) def search_person(search_strings, search_description=False): basic_query=Person.objects.select_related('books__book__alt_titles') for word in search_strings: basic_query=basic_query.filter(Q(first_name__icontains=word) | Q(last_name__icontains=word) | Q(id__icontains=word) | Q(books__book__isbn__icontains=word) | Q(books__book__title__icontains=word) | Q(books__book__alt_titles__alt_title=word) | Q(books__book__id__id__icontains=word)) return remove_duplicates(basic_query.all()) def commit(filename=None): if filename: try: f = file(filename, 'r') text = f.read() f.close() except IOError, e: print 'commit: Error reading file %s: %s' % (filename, e) print 'Exiting.' sys.exit(1) else: text = sys.stdin.read() actions = read_actionlist(text) commit_actions(actions) @transaction.commit_manually def commit_actions(actions): # Ensure that creation of new persons is performed first, so that # books in the same commit can refer to them: def action_cmp(a1, a2): if a1['action'] == 'new-person' and a2['action'] != 'new-person': return -1 return cmp(a1, a2) actions.sort(action_cmp) try: for action in actions: perform_action(action) transaction.commit() except Exception, e: print 'Error when commiting.' print e traceback.print_exc() transaction.rollback() def perform_action(a): if a['action'] == 'edit-book': b = Book.objects.get(isbn=a['isbn']) if 'title' in a: b.title = a['title'] if 'subtitle' in a: b.subtitle = a['subtitle'] if 'category' in a: b.category = Category.objects.get(id=a['category']) if 'publisher' in a: b.publisher = a['publisher'] if 'published_year' in a: b.published_year = a['published_year'] if 'edition' in a: b.edition = a['edition'] if 'num_pages' in a: b.num_pages = a['num_pages'] if 'series' in a: series_lst = BookSeries.objects.filter(Q(id=a['series'])) if len(series_lst) > 0: b.series = series_lst[0] else: b.series = None if 'description' in a: b.description = a['description'] if 'persons' in a: for bp in b.persons.all(): if bp.person.id not in a['persons'][bp.relation.name]: bp.delete() for rel, person_list in a['persons'].items(): for person_id in person_list: if person_id not in map(lambda p: p.id, b.get_persons(rel)): BookPerson(book=b, person=Person.objects.get(id=person_id), relation=Relation.objects.get(name=rel)).save() if 'references' in a: for ref in b.references.all(): if ref.text not in a['references'][ref.reference_type]: ref.delete() for reftype, text_list in a['references'].items(): for text in text_list: if text not in map(lambda r: r.text, b.references.all()): Reference(book=b, reference_type=ReferenceType.objects.get(name=reftype), text=text).save() # TODO pictures b.save() def edit_book_or_person(ids): filename = show_book_or_person(ids, commit_format=True, tmp_file=True) print filename run_editor(filename) commit(filename) commands = { 'show': { 'args': [('ids', (1,None))], 'options': ['commit_format', 'tmp_file'], 'fun': show_book_or_person }, 'search': { 'args': [('search_strings', (1,None))], 'options': ['search_description'], 'fun': search.search_book_cmd }, 'search-person': { 'args': [('search_strings', (1,None))], 'options': [], 'fun': search_person_cmd }, 'commit': { 'args': [('filename', (0,1))], 'options': [], 'fun': commit }, 'edit': { 'args': [('ids', (1,None))], 'options': [], 'fun': edit_book_or_person }, 'map': { 'args': [('shelfname', (0,1)), ('category', (0,1))], 'options': [], 'fun': placement.print_shelf_cmd } } flags = { 'commit_format': { 'help': 'output data in the format expected by the commit command' }, 'tmp_file': { 'help': 'output data to a new temporary file instead of to stdout' }, 'search_description': { 'help': 'include description field when searching' } } general_options = [] # options applicable to all commands class BadCommandLine(Exception): def __init__(self, msg): Exception.__init__(self, 'Bad command line: ' + msg) def check_command_args(args, command): cmd_decl = commands[command] min_num_args = sum(map(lambda a: a[1][0], cmd_decl['args'])) unlimited = any(map(lambda a: a[1][1] == None, cmd_decl['args'])) if not unlimited: max_num_args = sum(map(lambda a: a[1][1], cmd_decl['args'])) if len(args) < min_num_args: raise BadCommandLine('Too few arguments for command %s (expects at least %d, %d given).' % (command, min_num_args, len(args))) if (not unlimited) and (len(args) > max_num_args): raise BadCommandLine('Too many arguments for command %s (expects at most %d, %d given).' % (command, max_num_args, len(args))) def check_command_opts(opts, command): cmd_decl = commands[command] for opt,val in opts: if ((opt not in cmd_decl['options']) and (opt not in general_options)): raise BadCommandLine('Option %s not applicable to command %s.' % (opt, command)) def assign_command_args(args, command): cmd_decl = commands[command] d = {} i = 0 for param in cmd_decl['args']: pname = param[0] pmin = param[1][0] pmax = param[1][1] if pmax == 1: if i < len(args): d[pname] = args[i] i += 1 else: d[pname] = None else: d[pname] = [] j = 0 while i + j < len(args) and (pmax == None or j < pmax): d[pname].append(args[i + j]) j += 1 i = i + j return d def assign_command_opts(opts, command): d = {} for option, value in opts: if option in flags: d[option] = True else: d[option] = value return d def parse_cmdline(args): def getopt_option_name(internal_name): return internal_name.replace('_', '-') def internal_option_name(getopt_ret_name): return getopt_ret_name[2:].replace('-', '_') option_names = map(getopt_option_name, flags) options, args = getopt.getopt(args, '', option_names) if len(args) == 0: raise BadCommandLine('No command specified.') cmd_name = args[0] if cmd_name not in commands: raise BadCommandLine('Nonexisting command %s.' % cmd_name) cmd_decl = commands[cmd_name] cmd_args = args[1:] cmd_opts = map(lambda (opt,val): (internal_option_name(opt), val), options) check_command_args(cmd_args, cmd_name) check_command_opts(cmd_opts, cmd_name) return { 'command': cmd_name, 'args': combine_dicts(assign_command_args(cmd_args, cmd_name), assign_command_opts(cmd_opts, cmd_name)) } def invoke_command(command, args): cmd_decl = commands[command] cmd_decl['fun'](**args) def combine_dicts(*dicts): res = {} for d in dicts: res.update(d) return res def run_editor(filename): if os.path.exists(filename): os.system("%s %s || /usr/bin/env vi %s" %(os.getenv("EDITOR"), filename, filename)) else: exit("Error: %s: File does not exist!" % filename) def write_tmpfile(pfix='', content=''): file = tempfile.NamedTemporaryFile(prefix=pfix+'-', dir='/tmp', delete=False) file.write(content.encode(file_encoding)) name = file.name file.close() return name cmdline_parsed = parse_cmdline(sys.argv[1:]) print 'command line parsed to:', cmdline_parsed invoke_command(cmdline_parsed['command'], cmdline_parsed['args'])