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 NoneThese 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!]
