Previous tutorial links: one, two and three.
In the last section of the PiPassport project we covered building a class with functionality for saving and loading people's NFC entries, and saving and loading
what achievements they could pick up at each station.
After the last tutorial, your nfc.py file should be looking something like this:
import nxppy import os,json,requests from xml.dom import minidom class NFC(object): def __init__(self,pifile,peoplefile): self.pi=pifile self.peoplef=peoplefile self.achievements=self.LoadPi() self.people=self.LoadPeople() def ReadCard(self): id=nxppy.read_mifare() if id == None: return None else: if id not in self.people.keys(): print "new ID :", id name=raw_input('Please enter your name:') self.people[id]={"name":name,"achievements":[]} else: for aid in self.achievements.keys(): if aid not in self.people[id]['achievements']: if self.achievements[aid]['question']==None: self.people[id]['achievements'].append(aid) print "Achievement unlocked!" else: print self.achievements[aid]['question'] ans=[] for a in range(len(self.achievements[aid]['answers'])): answer=raw_input('Enter answer:') ans.append(answer) correct=0 for an in range(len(ans)): found=False for answ in self.achievements[aid]['answers']: if answ==ans[an]: found=True break if not found: print "answer " + str(ans[an]) + " incorrect!" else: correct+=1 if correct == len(self.achievements[aid]['answers']): print "achievement unlocked!" self.people[id]['achievements'].append(aid) self.SavePeople() return self.people[id] def Load(self,file,type): if os.path.exists(file): source=open(file) try: dom1=minidom.parse(source) except: impl=minidom.getDOMImplementation() dom1=impl.createDocument(None,type,None) return dom1 else: impl=minidom.getDOMImplementation() doc=impl.createDocument(None, type,None) return doc def LoadPi(self): dom=self.Load(self.pi,"piSyst") try: pi_tag=dom.getElementsByTagName("pi")[0] except: self.SavePi() dom=self.Load(self.pi,"piSyst") pi_tag=dom.getElementsByTagName("pi")[0] a_tags=pi_tag.getElementsByTagName("achievement") achievements={} achievements[pi_tag.getAttribute("ID")]={"question":None,"answers":None} for a in a_tags: id=a.getAttribute("ID") qtag=a.getElementsByTagName("question")[0] question=qtag.childNodes[0].data atag=a.getElementsByTagName("answer") answers=[] for an in atag: answers.append(an.childNodes[0].data) achievements[id]={"question":question,"answers":answers} return achievements def SavePi(self): dom=self.Load(self.pi,"piSyst") top=dom.documentElement if(len(dom.getElementsByTagName("pi"))==0): pi=dom.createElement("pi") id=0 pi.setAttribute("ID",str(id)) top.appendChild(pi) file=open(self.pi,'w') dom.writexml(file) else: old_achievements=self.LoadPi() pitag=dom.getElementsByTagName("pi")[0] if old_achievements != self.achievements: try: os.remove(self.pi) except Exception, e: print str(e) pass dom=self.Load(self.pi,"piSyst") top=dom.documentElement pitag=dom.createElement("pi") id=0 pitag.setAttribute("ID",str(id)) top.appendChild(pitag) for id,a in self.achievements.iteritems(): if a["question"]!=None: ac=dom.createElement("achievement") ac.setAttribute("ID",id) q=dom.createElement("question") text=dom.createTextNode(str(a["question"])) q.appendChild(text) ac.appendChild(q) for answer in a["answers"]: ans=dom.createElement("answer") txt=dom.createTextNode(str(answer)) ans.appendChild(txt) ac.appendChild(ans) pitag.appendChild(ac) file=open(self.pi,'w') dom.writexml(file) def LoadPeople(self): dom=self.Load(self.peoplef,"People") p_tags=dom.getElementsByTagName("person") people={} for p in p_tags: id=p.getAttribute("ID") ntag=p.getElementsByTagName("name")[0] name=ntag.childNodes[0].data atag=p.getElementsByTagName("achievement") achievements=[] for a in atag: if a.childNodes[0].data not in achievements: achievements.append(a.childNodes[0].data) people[id]={"name":name,"achievements":achievements} return people def SavePeople(self): dom=self.Load(self.peoplef,"People") top=dom.documentElement old_p=self.LoadPeople() people_tags=top.getElementsByTagName("person") for id,p in self.people.iteritems(): if id in old_p.keys(): for pe in people_tags: if pe.getAttribute("ID")==id: for achievement in p["achievements"]: if achievement not in old_p[id]["achievements"]: ac_tag=dom.createElement("achievement") ac_text=dom.createTextNode(achievement) ac_tag.appendChild(ac_text) pe.appendChild(ac_tag) else: peep=dom.createElement("person") peep.setAttribute("ID",id) n=dom.createElement("name") text=dom.createTextNode(str(p["name"])) n.appendChild(text) peep.appendChild(n) for achievement in p["achievements"]: ac=dom.createElement("achievement") txt=dom.createTextNode(str(achievement)) ac.appendChild(txt) peep.appendChild(ac) top.appendChild(peep) file=open(self.peoplef,'w') dom.writexml(file) def AddAchievement(self,desc,question,answers): return None def UpdateAchievement(self,updated_entry): return None def DeleteAchievement(self,id): return None def GetAchievement(self, id): if id in self.achievements.keys(): return self.achievements[id] else: return None
Now that we have the infrastructure done, we're going to do the final three method stubs which will allow us to do any modifications to any achievement.
As all of the file handling is done elsewhere, these are pretty simple additions:
def AddAchievement(self,desc,question,answers): ids=[int(i) for i in self.achievements.keys()] sorted_ids=sorted(ids) id=sorted_ids[-1]+1 self.achievements[str(id)]={"question":question,"answers":answers,"Description":desc} self.SavePi() def UpdateAchievement(self,updated_entry): if id in self.achievements.keys(): self.achievements[id]=updated_entry else: print "Achievement not found" def DeleteAchievement(self,id): if id in self.achievements.keys(): del self.achievements[id] else: print "Achievement not found"
From the top: Add simply finds out what the next ID is based on the highest ID: it does this by taking a list, that is, the ids of the achievements, sorting them ascending,
and then taking the final item (sorted_ids[-1]) and adding 1 on.
We then add the item and save the file.
Next there's update which is even simpler - finds the entry, and switches it for a different dictionary reference which gets filled in by the admin page.
Finally, delete checks the ID is there and removes it.
Now we can close nfc.py and make the adminy page: press CTRL+X, y and then enter.
Now enter sudo nano admin.py and put in this stub:
from nfc import NFC class UI(object): def __init__(self,pi,people): self.NFC=NFC(pi,people) self.CRUD={1:self.View,2:self.Create,3:self.Update,4:self.Delete,5:self.Quit} def Menu(self): print "Welcome to xyz admin." print "1. View Achievements" print "2. Add Achievements" print "3. Update Achievements" print "4. Remove Achievements" print "5. Quit" valid=False while not valid: input=raw_input('Enter an option:') validate=self.ValidateChoice(input) if validate == True: valid=True else: print validate self.CRUD[int(input)]() def View(self): return None def Create(self): return None def Update(self): return None def Delete(self): return None def Quit(self): return None def ValidateChoice(self,item): inte=self.ValidateInt(item) if not inte: return "Entry not an integer" if int(item) not in self.CRUD.keys(): return str(item)+ " not in list" else: return True def ValidateInt(self,item): try: val=int(item) return True except: return False def ProcessEntry(self,string): valid=False while not valid: id=raw_input('Please enter a valid ' +string+ ': ') valid=self.ValidateInt(id) if not valid: print "Invalid number" return id self=UI('pi.xml','people.xml') self.Menu()
Here we're making what's called a menu driven user interface: this is similar to the whole automated phone messages with "press 1 for this 2 for that and 3 to speak to a human",
except here it's much easier to work with as we can read it. We have all the options printed inside Menu(), and then let the user enter a choice.
If the user picks an invalid choice, this is is flagged by the ValidateChoice method, which checks the entry is an integer, and then checks whether the ID is listed in the CRUD dictionary.
The CRUD dictionary we've defined in the init (self.CRUD) is quite special, as instead of having data in it, it has a method attached to each ID. This is a pythonic way to replace the switch/case statement
that doesn't exist - in other languages like C, C++, C#, Java and a whole host of others, a switch case works like this:
value=user_input; switch(value){ case 0: DoSomething(); break case 1: DoSomethingElse(); break; default: DoSomethingIfAllOtherOptionsArentChosen(); break; }
so that the code can go down many different paths depending on what choice is made. Python doesn't have this, but our dictionary works just the same, and this gets called once we know the user input is valid.
The other method I haven't referenced - ProcessEntry - will get called whenever we need it - this enables us to do the repetitive task of asking the user for valid input until they actually put in something that we can use.
We can't use this in the menu as the entries not only have to be valid integers, they also have to be in a certain list, of which the other entries later on won't have to be.
At present though, when the user enters a choice the program will end as the methods don't do anything, so let's populate them:
def View(self): input=raw_input('Display all achievements? (y/n)') if input.lower()=='y': entries=self.NFC.achievements for id, a in entries.iteritems(): print "ID: ", id for id, entry in a.iteritems(): print id, ": ", entry else: id=self.ProcessEntry('Achievement ID') entry=self.NFC.GetAchievement(id) if entry is None: print "Achievement not found" else: for id, e in entry.iteritems(): print id, " : ", e def Create(self): num=int(self.ProcessEntry('number of achievements')) for i in range(num): desc=raw_input('Achievement description:') question=raw_input('Question:') vint=False ansint=int(self.ProcessEntry('number of answers')) answers=[] for i in range(ansint): answer=raw_input('Enter answer '+str(i)+':') answers.append(answer) self.NFC.AddAchievement(desc,question,answers)
The view method here either pulls out all of the achievements and iterates through each one, printing the various values inside the dictionary, or it can let the user select an achievement to view according to ID.
Create asks for a number of achievements using the magic ProcessEntry method, then asks for each entry for the achievement until all the achievements are made.
We have description in as a kind of stub at this point - this will get stored on the API, but if you're using this standalone, you could adjust the xml to store the description.
Moving on:
def Update(self): id=self.ProcessEntry('Achievement ID') entry=self.NFC.GetAchievement(id) new_e={} if entry is not None: for id in entry.keys(): if id != "answers": update=raw_input('Enter update for '+id+': ') new_e[id]=update else: answers=[] for a in entry["answers"]: u=raw_input('Enter update for answer '+a+': ') answers.append(u) new_e[id]=answers self.NFC.UpdateAchievement(new_e) print "Update successful!" else: print "Update failed" def Delete(self): id=self.ProcessEntry('Achievement ID') entry=self.NFC.GetAchievement(id) for key, e in entry.iteritems(): print key, ":", e yn=raw_input('Confirm delete? (y/n)') if yn.lower()=='y' or yn.lower()=="yes": self.NFC.DeleteAchievement(id) else: return None
These are pretty straight forward: update takes an entry to search for, goes off and tries to find it, and then takes in a new entry for each entry.
It would probably also be an idea to output the previous entry for each point:
if id != "answer": print "previous " +id+ ":", entry[id] update=raw_input('Enter update for '+id+': ')
Moving along, delete does a similar thing to view - pulls out an entry, outputs it and then asks for confirmation that you'd like to get rid of it.
These are all the methods we need to make a basic interface, and hopefully this file should now need very little modification as any updates to how data is loaded
and saved is handled by nfc.py.
If you now close this and run it, you should get a handy menu and be able to update and change achievements to your hearts content.
See next post, when I'll be moving away from the pi temporarily to explain how to set up a simple Web API using Azure: if you would like to use a different service, then please by all means skip that one and the final tutorial should still be easy to use and modify a little.
[PSST: if you're bored of copying and pasting, here's the github repo - it's probably easier due to python indents messing up, but do read, don't cheat and skip steps!]