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.

sc6.png

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