"""IMS.py Inventory Management System """ __author__ = "Josh English (Joshua.R.English@gmail.com)" __version__ = "$Revision: 1.03 $" __date__ = "$Date: 2006/07/14 15:55:42 $" __copyright__ = "Copyright (c) 2006 Josh English" __license__ = "Python" __history__ = """History: 1.01 (Jan 9, 2006) Adding a submission to a story also changes the story's status to OnMarket 1.02 (March, 2006) Adding a submission to a story only changes status to OnMarket if previous status was 'draft' 1.03 (Jul 14, 2006) Adding a submission to a story that is sold changes the status to Reprint """ from xml.dom import minidom from datetime import date import time import os class MethodException(Exception): pass class DateException(Exception): pass class StoryError(Exception): pass class SubmissionError(Exception): pass ###Constants subfile = "Marvin:Documents:Fiction:subs.xml" storyfile = "Marvin:Documents:Fiction:stories.xml" marketfile = "Marvin:Documents:Ficiton:markets.xml" submissionStatusCodes =['InMail','Reject','Sale'] storyStatusCodes = ['Treatment','Draft','OnMarket','Sold','Reprint','Retired'] marketdict={ 'WotF':'WotF', 'FSF':'FSF', 'Analog':'Anlg', 'Asimovs':'Asmv', 'Andromeda':'ASIM', 'Gateway':'Gway', 'StrangeHorizons':'SHrz', 'Lenox':'LxAv', 'Talebones':'TlBn', 'Ideomancer':'Ideo', 'AlienSkin':'Skin', 'SonAndFoe':'SFoe', 'LadyChurchills':'LCRW', 'Apex':'Apex', 'Polyphony':'Poly', 'Byzarium':'Bzrm', 'WierdTales':'WdTl', "LeadingEdge":'Edge', "LoneStar":'Lone', "Neo-opsis":'NeoO', "Flytrap":'FlyT', "FicticiousForce":'FiFo', "Shimmer":'Shmr', 'QuantumMuse':'Muse', "RealmsOfFantasy":'Rof_', "OnSpec":'OnSp', "FUHU":'FUHU', "Abyss":'Abys', "Baen":'Baen', "StrangerBox":'SBox', 'EscapePod':'EPod', 'OSCIM':'Card', "Fantasy":'Fant', "DarkEnergy":'Dark', "ChaosTheory":'Chos', "Atomjack":'Atom', "Grendelsong":'Gsng', "Noneuclidean Cafe":'Cafe', "Cafe Irreal": 'CfIr', "Flash Me": 'FlsM', "DarkRecesses":'DkRc', "SandAndSea":'SSea', "Flashquake":'FlQk', "Gryphonwood":'Gryf', } marketcodes = marketdict.keys() submissionMethods=['mail','email'] ### Date procedures dateformats = ['%b %d, %Y','%b %d, %y','%m %d %Y','%Y %m %d','%m/%d/%y','%m-%d-%y'] preffereddateformat = dateformats[0] def StringToDate(ts): """StringToDate(string) Takes a string and returns a Date object """ for df in dateformats: try: t = time.strptime(ts,df) return date(t[0],t[1],t[2]) except: pass raise "Cannot parse date string" WORDOS = 'wordos' NINA = 'nina' LEGUIN = 'leguin' CRITTERS = 'critters' WELORIANS = 'welorians' workshops = [WORDOS,NINA,LEGUIN,CRITTERS,WELORIANS] ### ANYTHING YOU WOULD WANT TO CUSTOMIZE SHOULD BE ABOVE THIS LINE ### Customized Filers class Filer: def __init__(self,path,tag="story",key="code"): self.SourcePath = path ### source of the original data self.TagName = tag ### tag to search for 'story','submission', etc. self.Keyword = key ### unique keyword to control data docElement = minidom.parse(path) self.Main = docElement.firstChild self.SourceName = self.Main.tagName ### saved for the Save() method self._dict={} self.DataDirectory = os.path.join(os.getcwd(),"Data") if not os.path.exists(self.DataDirectory): os.makedirs(self.DataDirectory) for item in self.Main.getElementsByTagName(tag): ### Search for an attribute key codekey = str(item.getAttribute(key)) ### Otherwise look for text inside a subelement with name key if not codekey: codekey = str(item.getElementsByTagName(key)[0].firstChild.data.strip()) filename = "%s--%s" % (self.TagName,codekey) filename = os.path.join(self.DataDirectory,filename) out = open(filename,'w') out.write(item.toxml()) out.close() self._dict[codekey]=filename #self._dict[codekey].write(item.toxml()) # def __del__(self): # for k in self._dict.keys(): # self._dict[k].close() def keys(self): return self._dict.keys() def getData(self,codekey): """returns the text of the file""" out = file(self._dict[codekey],'r') return out.read() def getXML(self,codekey): """returns the minidom element represented by the file.""" return minidom.parseString(self.getData(codekey)).firstChild def __getitem__(self,key): """Returns a link to the open file""" return self._dict[key] def getFile(self,key): return self._dict[key] def has_key(self,item): return self._dict.has_key(item) def replace(self,codekey,data): """Replace the old codekey with a new minidom element""" if data.tagName != self.TagName: raise "Cannot replace data" #self._dict[codekey].close() ### gets rid of old file (loses data) !!!!!! #self._dict[codekey]=NamedTemporaryFile() ### make a new file out = file(self._dict[codekey],'w') out.write(data.toxml()) def Save(self): """Saves the data to the original path""" from time import asctime e = minidom.Element(self.SourceName) e.setAttribute('mod_date',asctime()) e.setAttribute('path',self.SourcePath) NL = minidom.Text() NL.data =u"\n" e.appendChild(NL) for key in self.keys(): x = self.getXML(key) e.appendChild(x) NL = minidom.Text() NL.data =u"\n" e.appendChild(NL) dump = open(self.SourcePath,'w') dump.write(e.toxml()) dump.close() #print e.toxml() def New(self,codekey,text): """New(codekey,text) Writes the text into a new file """ if self._dict.has_key(codekey): raise "Cannot create new file, duplicate code %s" % codekey filename = "%s--%s" % (self.TagName,codekey) filename = os.path.join(self.DataDirectory,filename) out = open(filename,'w') out.write(text) out.close() self._dict[codekey]=filename #self._dict[codekey]=NamedTemporaryFile() #self._dict[codekey].write(text) class StoryFiler(Filer): def __init__(self): Filer.__init__(self,storyfile,"story","code") import os.path nm,ext = os.path.splitext(storyfile) bu = nm+"BACKUP"+ext b = open(bu,"w") o = open(storyfile,"r") b.write(o.read()) o.close() b.close() # def __del__(self): # for key in self.keys(): self[key].close() def getStory(self,key): return NodeToStory(self.getXML(key)) def Save(self): from time import asctime e = minidom.Element(self.SourceName) e.setAttribute('mod_date',asctime()) e.setAttribute('path',self.SourcePath) for key in self.keys(): x = self.getXML(key) e.appendChild(x) dump = open(self.SourcePath,'w') p = Printer(e) dump.write(p.write()) dump.close() class SubFiler(Filer): def __init__(self): Filer.__init__(self,subfile,"submission","code") def getSubmission(self,key): return NodeToSubmission(self.getXML(key)) def Save(self): from time import asctime e = minidom.Element(self.SourceName) e.setAttribute('mod_date',asctime()) e.setAttribute('path',self.SourcePath) for key in self.keys(): x = self.getXML(key) e.appendChild(x) dump = open(self.SourcePath,'w') p = Printer(e) dump.write(p.write()) dump.close() ### Tools for dealing with XML def toNode(nodeName,data): """returns a minidom element object with text child of data n = toNode('flash','fiction') n.toxml() = "fiction" """ text = minidom.Text() text.data=str(data) node = minidom.Element(nodeName) node.appendChild(text) return node class Printer: """Printer(element,newl="\n",forceindent=False) Returns a text representation of a minidom element. This is more readable than the minidom.element.toxml method and more compact than the minidom.element.toprettyxml method. Does not handle all types of xml. Comments and processing instructions are ignored. """ def __init__(self,element,newl="\n"): import StringIO self.writer = StringIO.StringIO() self.newl = newl self.TopNodeName = element.nodeName self._print(element) def _print(self,sub,indent=""): """sub is an xml element""" self.writer.write(indent) if sub.nodeName == "#text": self.writer.seek(-len(indent),2) self.writer.write ("%s" % sub.data.strip()) elif sub.nodeType == sub.ELEMENT_NODE: if sub.parentNode: backup = sub.parentNode.nodeName == self.TopNodeName else: backup = True #if backup: self.writer.seek(-len(indent),2) self.writer.write("<%s" % (sub.nodeName)) atts = sub._get_attributes() if atts: atts = ' '+' '.join(['%s="%s"' % (k,v) for k,v in atts.items()]) else: atts = '' self.writer.write(atts) if sub.childNodes: self.writer.write(">") if len(sub.childNodes) > 1: self.writer.write(self.newl) #if backup : newindent = indent #else: newindent = indent+'\t' newindent = indent+'\t' for node in sub.childNodes: self._print(node,newindent) ### Possible comment out next two lines if len(sub.childNodes) > 1: self.writer.write(indent) #if backup: self.writer.seek(-(len(indent)),2) self.writer.write("%s" % (sub.nodeName,self.newl)) else: self.writer.write("/>%s" % (self.newl)) def write(self): return self.writer.getvalue() ### Story Class class Story: def __init__(self,code,title,wordcount,status,file,*args,**kw): self.code = str(code).lower() self.title = str(title) self.wordcount = int(wordcount) if status in storyStatusCodes: self.status = status else: raise StoryError, "Invalid Status %s" % status self.file = str(file) self.workshops = [] for a in args: if a in workshops: self.workshops.append(a) else: raise StoryError, "Workshop %s not recognized" % a self.pasttitles = [] self.history = [] self.submissions = [] self.plot = "" for k,v in kw.items(): if k == "pasttitles": self.pasttitles.extend(v) if k == "plot":self.plot = str(v) def addHistory(self,dobj,info): """addHistory(dobj,info) Adds history with a date object and info string """ self.history.append('%s: %s' % (dobj.strftime(preffereddateformat),info)) def newTitle(self,title): self.pasttitles.append(self.title) self.title=title def addSubmission(self,subcode): ### Add the submission, and change status to OnMarket self.submissions.append(subcode) ### Only change status if 'draft' or 'Sold' if self.status == 'Draft': self.status = 'OnMarket' if self.status == 'Sold' : self.status = 'Reprint' def toElement(self): e = minidom.Element('story') e.setAttribute('code',self.code) e.appendChild(toNode('title',self.title)) for pt in self.pasttitles: e.appendChild(toNode('pasttitle',pt)) e.appendChild(toNode('wordcount',self.wordcount)) e.appendChild(toNode('status',self.status)) for w in self.workshops: e.appendChild(minidom.Element(w)) e.appendChild(toNode('file',self.file)) h = minidom.Element('history') for item in self.history: h.appendChild(toNode('item',item)) e.appendChild(h) for sub in self.submissions: e.appendChild(toNode('submission',sub)) e.appendChild(toNode('plot',self.plot)) return e def toXML(self,newl="\n"): p = Printer(self.toElement(),newl) return p.write() def NodeToStory(node): code = node.getAttribute('code') title = node.getElementsByTagName('title')[0].firstChild.data.strip() pasttitles = [str(d.firstChild.data) for d in node.getElementsByTagName('pasttitle')] wordcount = node.getElementsByTagName('wordcount')[0].firstChild.data.strip() status = node.getElementsByTagName('status')[0].firstChild.data.strip() shops = filter(node.getElementsByTagName,workshops) file = node.getElementsByTagName('file')[0].firstChild.data items = [str(d.firstChild.data) for d in node.getElementsByTagName('item')] submissions = [str(d.firstChild.data) for d in node.getElementsByTagName('submission')] plots = node.getElementsByTagName('plot')[0] if plots.childNodes: plot = plots.firstChild.data else: plot = '' filltitles = False if pasttitles: usefirst = pasttitles[0] pasttitles.pop(0) filltitles = True else: usefirst = title #code,title,wordcount,status,file,*args,**kw s = Story(code,usefirst,wordcount,status,file) if filltitles: for pt in pasttitles: s.newTitle(pt) s.newTitle(title) s.workshops = shops for item in items: s.addHistory(StringToDate(item[:12]),item[13:].strip()) for sub in submissions: s.addSubmission(sub) s.plot = plot return s MAIL = 'mail' EMAIL = 'email' class Submission: def __init__(self,story,storycode,market,date,method,cost,note=''): self.closed = False self.story =story self.storycode = storycode if market not in marketcodes: raise SubmissionError, "Unknown market %s" % market else: self.market = market self.datesent = StringToDate(date).strftime(preffereddateformat) self.code = "%s%s" % (marketdict[self.market],StringToDate(date).strftime('%Y%m%d')) if method not in submissionMethods: raise SubmissionError, "Unknown submission method %s" % method else: self.method = method self.cost = float(cost) if self.method == EMAIL and self.cost != 0: raise SubmissionError, "Cannot have cost to email submission" self.status = 'InMail' self.note = note def HowLong(self): """Returns the number of days a submission was out (or is currently out)""" if hasattr(self,'daysout'): return self.daysout else: do = date.today()-StringToDate(self.datesent) return do.days def Close(self,date,status,note=""): db = StringToDate(date) ds = StringToDate(self.datesent) do = db-ds if do.days<0: raise SubmissionError, "Cannot close sub before sent date" if self.closed: raise SubmissionError, "Submission already closed" self.dateback = StringToDate(date).strftime(preffereddateformat) self.daysout = do.days if status not in submissionStatusCodes: raise SubmissionError, "Unknown status code %s" % status else: self.status = status self.note += " %s" % (note) self.closed = True def toElement(self): e = minidom.Element('submission') e.appendChild(toNode('code',self.code)) e.appendChild(toNode('story',self.story)) e.appendChild(toNode('storycode',self.storycode)) e.appendChild(toNode('market',self.market)) e.appendChild(toNode('datesent',self.datesent)) if self.closed: e.appendChild(toNode('dateback',self.dateback)) e.appendChild(toNode('daysout',self.daysout)) e.appendChild(toNode('status',self.status)) e.appendChild(toNode('method',self.method)) e.appendChild(toNode('cost',"%.2f" % (self.cost))) if self.note: e.appendChild(toNode('note',self.note)) return e def NodeToSubmission(node): code = node.getElementsByTagName('code')[0].firstChild.data story = node.getElementsByTagName('story')[0].firstChild.data storycode = node.getElementsByTagName('storycode')[0].firstChild.data market = node.getElementsByTagName('market')[0].firstChild.data datesent = node.getElementsByTagName('datesent')[0].firstChild.data cost = node.getElementsByTagName('cost')[0].firstChild.data method = node.getElementsByTagName('method')[0].firstChild.data status = node.getElementsByTagName('status')[0].firstChild.data notes = node.getElementsByTagName('note') if notes: if notes[0].childNodes: note = notes[0].firstChild.data else: note = '' else: note = '' closed = False if status != "InMail": ### Then it is closed closed = True dateback = node.getElementsByTagName('dateback')[0].firstChild.data daysout = node.getElementsByTagName('daysout')[0].firstChild.data s = Submission(story,storycode,market,datesent,method,cost,note) if closed: s.Close(dateback,status) return s ########## RED LINE OF DEATH #######################