You are on page 1of 43

12/6/2014

London Python developer/AWS devop engineer

MAY
MAR

LondonPythondeveloper/AWSdevop
engineer

HowKickstarterwillkillOpenSource
AnyonewhoworkswithLinuxknowshowvaluableandrevolutionarytheopensourcemovementhas
been.Countlesshackersaroundtheworldworkingoftenfornothingonalltypesofsoftware.
YesterdayIsawaKickstarterprojectaskingfordonationstoaddschemamigrationstoDjangoscore
(http://www.kickstarter.com/projects/andrewgodwin/schemamigrationsfordjango).A2,500targetto
addsomenewfeaturestoDjango.Atthetimeofwritingpledgesforover12,000havebeenmade.The
troubleis,IthinkthiswilldamageDjangoandotheropensourceprojectsinthelongrun.
Yousee,Inearlydonated.IlikeDjango,Iveworkedwithitforseveralyears,anddatabasemigrationsare
apaininmostlanguages.ButInearlydonatedbecauseIlikeDjango.Thiswouldhavebeenawayof
showingiteventhoughImnotworkingwithitrightnow.Onlytheproblemisthatthedeveloperwhoset
uptheKickstarterprojectisfundinghisownworkonDjango.Whyisthataproblem?Shouldntpeople
getpaidtoworkonprojects?Wellyes,Ihavenothingagainstthat,and2,500fortheamountofwork
involvedseemsreasonable.Butwhen12,000+hasbeenpledged,wherewillthesurplusgo?Arecore
developerslikelytobeannoyedbythefactthatthisprojecthasraisedsomuchbeyondwhatwasaskedfor,
andwilltheyfeelentitledtosome?Stretchgoalsgoupto7,000,butnowafurther5,000hasbeen
pledgedontopofthat.Isthisasituationwherethefirstdeveloperfromaprojectgarnerslotsofloveand
makesagoodprofitforbeingthefirsttocomeupwiththeideaofrequestingfunding?Shouldtheexcess
becontributedbacktothecommunity/coredevteam?
IfIwereacoreDjangodeveloperwhoworkedontheprojectinmyowntime,Imightbethinkingaround
nowthatperhapsIshouldstartcreatingaprojectformyselfonKickstarter.Infact,perhapsImreasonably
owedsomemoneyforalltheworkIveputintoitinthepast.AndthisisthepartthatIthinkwillhurt
Django,andbyextensionotheropensourceprojects.
Thequestionofwhathappenstothesurplusisreallyirrelevant,andthisisonespecificproject.Theideais
interesting.Ifthisweretohappenondifferentsoftwareprojects,Ithinkwemightfindmoreofahesitancy
onthepartofdeveloperstoimplementcertainlargepiecesofwork,butperhapsmoreimportantly,
resentmentbetweenthosewhochargeandthosewhodont.
AlternativelyitcouldbearguedthatcreatingKickstarterprojectstofunddevelopmentwillultimatelylead
tomoreimportantworkbeingprioritisedbecausepeoplecanaffordtotakeabreakfromtheirdayjobsto
workonthosefeaturesimportantenoughtoothersthatpeoplewillfundthework.Isthismodel
sustainable?Whatdoyouthink?Isthisthebeginningoftheendofworkingforfreeonopensource
projects?Letmeknowinyourcomments
http://10kblogger.wordpress.com/

1/43

12/6/2014

London Python developer/AWS devop engineer

COMMENTS4Comments
CATEGORIESUncategorized

ARESTfulpasswordlockerwithDjangoand
backbone.jspart6
Thisseries(http://10kblogger.wordpress.com/2012/05/29/arestfulpasswordlockerwithdjangoand
backbonejspart5/)hasexplainedhowtocreateaRESTfulwebapplicationusingDjangoREST
frameworkandBackbone.js.Thecodeisavailable(https://github.com/boosh/pwlocker)foryoutoexplore
andplaywith.
Toconclude,Idliketodiscusswhatotheradditionswecouldmaketomaketheapplicationmoresecure,
performantandrobust.

Security
StoringpasswordsinplaintextinadatabaseisaBadIdea.Djangohashesuserpasswordsforus,sothose
credentialsarefine.Buthowcouldwesecurethepasswordsuserswanttoputinourpasswordlocker?
Ifwedidnthavetherequirementtoallowpasswordstobeshared,wecoulduseasymmetrickey
encryptionalgorithmwiththeusersrawauthenticationpasswordasasecretkeypossiblymungedwith
someextradata.Thiswouldmeanthatpasswordswouldonlybeabletobedecryptedonceauserlogged
inandwouldmakelargescalebruteforcingofthedatabaseunfeasibleifwechoseouralgorithmcarefully
sinceeveryuserspasswordwouldneedtobecrackedtodecrypttheirdata.Wewouldbestoringuser
passwordsinmemoryanditspossibletheycouldleaktosomedegree,butitdbesaferifahackerwas
onlyabletodownloadadumpofthedatabase.
Onepossibilityforsupportingsharingandmakingthestoreddatamoresecurewouldbetousepublickey
cryptography.Theprivatekeycouldrequiretheuserspasswordtodecryptdata.Ifausersharesa
password,wecouldencryptitwiththerecipientspublickeyandtheydbeabletodecryptitwiththeir
privatekeywhentheylogin.
Cryptographyiscomputationallyexpensive,andsinceourcodeisinpythonwemayfinditbettertocode
thesemodulesinacompiledlanguage.SomepythonlibrariesimplementtheirencryptionroutinesinC,so
wecouldusethese.However,ifwewereinterestedinscalability,wemayfinditmoreperformanttouse
dedicatedserverstohandlethecryptography.Inthisscenario,wecoulduseanRPCframeworksuchas
ApacheThrift(http://thrift.apache.org/)tohandlecommunicationbetweenthefrontendwebnodesand
Java/Cbackends.

http://10kblogger.wordpress.com/

2/43

12/6/2014

London Python developer/AWS devop engineer

Also,theentireapplicationmustrunoverSSLforthesitetobesecure,andideallynotcontainanythird
partycontent(suchasadverts)tomakesurethattheresnopossibilityforsomekindofcrossdomain
Javascriptbugtostealuserpasswords.

Performanceandrobustness
Beforeputtingthiscodeintoproduction,weshouldcreateafullsuiteofunit&functionaltests.Italso
needstestingcrossbrowsertestingtomakesuretherearenoquirksindifferentbrowsers.
BecausethemajorityoftheapplicationisloadedviaAJAX,wecancachewebtemplatestoalargedegree
whichwillreduceloadonthewebnodes.Ofcourse,weshouldalsocombineandminifyJavascriptand
CSS.
Thatsall.Ihopeyouvefoundthistutorialuseful.
COMMENTSLeaveaComment
CATEGORIESProgramming

ARESTfulpasswordlockerwithDjangoand
backbone.jspart5
Inthispenultimatepartofthisseries(http://10kblogger.wordpress.com/2012/05/28/arestfulpassword
lockerwithdjangoandbackbonejspart4/),weregoingtoaddtheabilitytosharepasswordsbetween
users.Thespecforsharingpasswordsisasfollows:
1. Usersshouldbeabletomaintainacontactlistofotheruserswithwhomtheycansharepasswords.
Usersmustbeabletosearchforotherusersbyusername,andbeabletoviewtheirfirstandlastname
toconfirmtheuseriswhotheythinktheyare.
2. Ifauserremovesanotheruserfromtheircontactlist,allpasswordssharedwiththatusershouldstop
beingsharedwiththeremovedcontact.
3. Passwordsmustbeabletobesharedwithmultipleusersinthecreatorscontactlist.
4. Onlythecreatorofapasswordisallowedtomodifydata.Userswithwhomitssharedhavereadonly
access.
5. Onlythecreatorofapasswordmayshareapassword(i.e.ifUserAhassharedapasswordwithUser
B,UserBmaynotsharethatpasswordwithanyoneelse).
SincethisseriesisprimarilyaboutBackbone.js,wellimplementtheaboveinasinglepagedwebapp.

Thecontactlist
http://10kblogger.wordpress.com/

3/43

12/6/2014

London Python developer/AWS devop engineer

Toallowuserstosharepasswords,wellcreateamanytomanyrelationshiptoanewmodel.Update
`apps/passwords/models.py`tothefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

fromdjango.dbimportmodels
fromdjango.contrib.auth.modelsimportUser

classPassword(models.Model):
"""
Representsausernameandpasswordtogetherwithseveralothe
"""
created_by=models.ForeignKey(User,related_name='+',editab
title=models.CharField(max_length=200)
username=models.CharField(max_length=200,
blank=True)
password=models.CharField(max_length=200)
url=models.URLField(max_length=500,
blank=True,
verbose_name='SiteURL')
notes=models.CharField(
max_length=500,
blank=True)
created_at=models.DateTimeField(auto_now_add=True,editable
updated_at=models.DateTimeField(auto_now=True,editable=
shares=models.ManyToManyField('PasswordContact',
verbose_name='Sharewith',blank=True)

def__unicode__(self):
returnself.title

classPasswordContact(models.Model):
"""
SomeonewithwhomausercanshareaPassword
"""
from_user=models.ForeignKey(User,related_name="passwordcon
to_user=models.ForeignKey(User,related_name="passwordconta
created_at=models.DateTimeField(auto_now_add=True,editable
updated_at=models.DateTimeField(auto_now=True,editable=

def__unicode__(self):
return"%s%s(%s)"%(self.to_user.first_name,self.to_u

ThisisstandardDjango.Migratewithsouthandapplyit:`./manage.pyschemamigrationpasswordsauto
&&./manage.pymigratepasswords`.
Wellcreate2newAPIsoneforthePasswordContactresource,andanotherforUserobjectswhichwill
allowmemberstosearchforotherusers.
Create`apps/users/resources.py`andenterthefollowing:
1
2
3
4

fromdjangorestframework.resourcesimportModelResource

fromdjango.contrib.auth.modelsimportUser

http://10kblogger.wordpress.com/

4/43

12/6/2014

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

London Python developer/AWS devop engineer

classUserResource(ModelResource):
"""
Letsuserssearchforotherusersbyusername.
"""
model=User
fields=('id','first_name','last_name','username','url'

defvalidate_request(self,data,files=None):
"""
Backbone.jswillsubmitallfieldsinthemodelbacktou
somefieldsaresetasuneditableinourDjangomodel.So
toremovethoseextrafieldsbeforeperformingvalidation
"""
forkeyinself.ignore_fields:
ifkeyindata:
deldata[key]

returnsuper(UserResource,self).validate_request(data,f

Update`apps/passwords/resources.py`tothefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

fromdjangorestframework.resourcesimportModelResource
fromdjangorestframework.serializerimportSerializer
fromdjango.core.urlresolversimportreverse

fromapps.users.resourcesimportUserResource
frommodelsimportPassword,PasswordContact

classPasswordContactResource(ModelResource):
model=PasswordContact
ordering=('to_user__first_name',)
fields=('id','url',('to_user','UserResource'),('from_us
ignore_fields=('id',)

defvalidate_request(self,data,files=None):
"""
Backbone.jswillsubmitallfieldsinthemodelbacktou
somefieldsaresetasuneditableinourDjangomodel.So
toremovethoseextrafieldsbeforeperformingvalidation
"""
forkeyinself.ignore_fields:
ifkeyindata:
deldata[key]

returnsuper(PasswordContactResource,self).validate_requ

classCurrentUserSingleton(object):
"""
LiterallytheonlywayIcanfindtogivethePasswordResourc
tothecurrentuserobject.
"""
user=None

@classmethod
defset_user(cls,user):
cls.user=user

http://10kblogger.wordpress.com/

5/43

12/6/2014

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

London Python developer/AWS devop engineer

classPasswordResource(ModelResource):
model=Password
#bydefault,djangorestframeworkwon'treturntheIDbac
#needsitthough,sodon'texcludeit
exclude=('created_by',)
ordering=('title',)
#djangorestframeworkwilloverwriteour'url'attributewi
#thatpointstotheresource,soweneedtoprovideanalter
include=('resource_url',)
ignore_fields=('created_at','updated_at','id','maskedPas
'resource_url','is_owner')
fields=('id','title','username','password','url','note
'resource_url','shares','is_owner')

related_serializer=PasswordContactResource

defis_owner(self,instance):
"""
ReturnsTrueifthisresourcewascreatedbythecurrent
"""
returninstance.created_by==CurrentUserSingleton.user

defurl(self,instance):
"""
ReturntheinstanceURL.Ifwedon'tspecifythis,django
frameworkwillreturnageneratedURLtotheresource
"""
returninstance.url

defresource_url(self,instance):
"""
Analternativetothe'url'attributedjangorestframewo
addtothemodel.
"""
returnreverse('passwords_api_instance',
kwargs={'id':instance.id})

defvalidate_request(self,data,files=None):
"""
Backbone.jswillsubmitallfieldsinthemodelbacktou
somefieldsaresetasuneditableinourDjangomodel.So
toremovethoseextrafieldsbeforeperformingvalidation
"""
forkeyinself.ignore_fields:
ifkeyindata:
deldata[key]

returnsuper(PasswordResource,self).validate_request(dat

Change`apps/api/urls.py`tothefollowingtowireupURLs:
1
2
3
4

fromdjango.conf.urls.defaultsimportpatterns,url

fromviewsimportPasswordListView,PasswordInstanceView
fromviewsimportPasswordContactListView,PasswordContactReadOrD

http://10kblogger.wordpress.com/

6/43

12/6/2014

5
6
7
8
9
10
11
12
13
14
15

London Python developer/AWS devop engineer

fromviewsimportUserView

urlpatterns=patterns('',
url(r'^passwords/$',PasswordListView.as_view(),name='passwo
url(r'^passwords/(?P[09]+)$',PasswordInstanceView.as_view()
url(r'^passwordcontacts/$',PasswordContactListView.as_view()
name='password_contacts_api_root'),
url(r'^passwordcontacts/(?P[09]+)$',PasswordContactReadOrDe
name='password_contacts_api_instance'),
url(r'^user/(?P.+)$',UserView.as_view(),name='user_api'),
)

Wealsoneedtoupdatetheviewsin`apps/api/views.py`:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

fromdjango.db.modelsimportQ
fromdjangorestframework.mixinsimportModelMixin,InstanceMixin
ReadModelMixin,DeleteModelMixin
fromdjangorestframework.permissionsimportIsAuthenticated
fromdjangorestframework.responseimportErrorResponse
fromdjangorestframeworkimportstatus
fromdjangorestframework.viewsimportListOrCreateModelView,Ins

fromapps.passwords.modelsimportPasswordContact
fromapps.passwords.resourcesimportPasswordResource,PasswordC
CurrentUserSingleton
fromapps.users.resourcesimportUserResource

classRestrictPasswordToUserMixin(ModelMixin):
"""
Mixinthatrestrictsuserstoworkingwiththeirowndata
"""
defget_queryset(self):
"""
Onlyreturnobjectscreatedby,orsharedwith,thecurr
authenticateduser.
"""
returnself.resource.model.objects.filter(Q(created_by
Q(shares__to_user=self.user)).distinct()

defget_instance_data(self,model,content,**kwargs):
"""
Setthecreated_byfieldtothecurrentlyauthenticated
"""
content['created_by']=self.user
returnsuper(RestrictPasswordToUserMixin,self).get_inst

definitial(self,request,*args,**kwargs):
"""
Setthecurrentlyauthenticateduserontheresource
"""
CurrentUserSingleton.set_user(request.user)
returnsuper(ModelMixin,self).initial(request,*args,

deffinal(self,request,response,*args,**kargs):
"""
Clearthecurrentusersingletontomakesureitdoesn't

http://10kblogger.wordpress.com/

7/43

12/6/2014

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

London Python developer/AWS devop engineer

"""
CurrentUserSingleton.set_user(None)
returnsuper(ModelMixin,self).final(request,response,

classPasswordListView(RestrictPasswordToUserMixin,ListOrCreate
"""
ListviewforPasswordobjects.
"""
resource=PasswordResource
permissions=(IsAuthenticated,)

classPasswordInstanceView(RestrictPasswordToUserMixin,Instance
"""
ViewforindividualPasswordinstances
"""
resource=PasswordResource
permissions=(IsAuthenticated,)

defput(self,request,*args,**kwargs):
"""
Onlyallowthecreatingusertomodifyaninstance.
"""
model=self.resource.model
query_kwargs=self.get_query_kwargs(request,*args,

try:
self.model_instance=self.get_instance(**query_kwar

ifself.model_instance.created_by==self.user:
returnsuper(RestrictPasswordToUserMixin,self
exceptmodel.DoesNotExist:
pass

raiseErrorResponse(status.HTTP_401_UNAUTHORIZED,None

defdelete(self,request,*args,**kwargs):
"""
Onlythecreatorshouldbeabletodeleteaninstance.
"""
model=self.resource.model
query_kwargs=self.get_query_kwargs(request,*args,

try:
instance=self.get_instance(**query_kwargs)
exceptmodel.DoesNotExist:
raiseErrorResponse(status.HTTP_404_NOT_FOUND,None

ifinstance.created_by==self.user:
instance.delete()
else:
raiseErrorResponse(status.HTTP_401_UNAUTHORIZED,

classPasswordContactListView(ListOrCreateModelView):
"""
ListviewforPasswordContactobjects.
"""
resource=PasswordContactResource

http://10kblogger.wordpress.com/

8/43

12/6/2014

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

London Python developer/AWS devop engineer

permissions=(IsAuthenticated,)

defget_queryset(self):
"""
Onlyreturnobjectswherethefrom_useristhecurrently
"""
returnself.resource.model.objects.filter(from_user=self

defget_instance_data(self,model,content,**kwargs):
"""
Setthefrom_userfieldtothecurrentlyauthenticatedu
"""
content['from_user']=self.user
returnsuper(PasswordContactListView,self).get_instance

classReadOnlyInstanceModelView(InstanceMixin,ReadModelMixin,M
"""
Aviewwhichprovidesdefaultoperationsforread/deleteaga
butthatpreventsupdates.
"""
_suffix='Instance'

classPasswordContactReadOrDeleteInstanceView(ReadOnlyInstanceMo
"""
ViewforindividualPasswordContactinstances
"""
resource=PasswordContactResource
permissions=(IsAuthenticated,)

defdelete(self,request,*args,**kwargs):
"""
DeletessharesfromPasswordswhenaPasswordContactis
"""
model=self.resource.model
query_kwargs=self.get_query_kwargs(request,*args,

try:
instance=self.get_instance(**query_kwargs)
exceptmodel.DoesNotExist:
raiseErrorResponse(status.HTTP_404_NOT_FOUND,None

#removeanysharesfromanypasswordssharedwiththis
password_contacts=PasswordContact.objects.filter(from_
to_user=instance.to_user)

forpassword_contactinpassword_contacts:
password_contact.delete()

instance.delete()
return

classUserView(InstanceMixin,ReadModelMixin,ModelView):
"""
ViewforindividualUsersletsusersfindotherusersbyuse
"""
resource=UserResource
permissions=(IsAuthenticated,)

http://10kblogger.wordpress.com/

9/43

12/6/2014

157
158
159
160
161
162
163

London Python developer/AWS devop engineer

defget_queryset(self):
"""
Filterthecurrentuserfromsearchresultstopreventt
withthemselves.
"""
returnself.resource.model.objects.filter(~Q(id=self.use

Finally,
Theresquitealotgoingonintheabovecode:
Wererestrictinguserstoonlyviewingthoseobjectsforwhichtheyrethecreatororarecipientofa
share.
AsingletonisusedtoenablethePasswordResourcetodeterminewhetherthecurrentlyauthenticated
usercreatedaresourceornot,andthisisreturnedasthe`is_owner`propertyweaddedtothe
PasswordResource.
WerestrictCRUDoperationsonpasswordinstancessotheycanonlybeperformedbythecreatorofa
password.
Userscanonlycreateordeletepasswordcontacts,theycantupdatethem.Whenapasswordcontactis
deleted,weremoveallsharesassociatedwiththatuser.Sowhenauserremovessomeonefromtheir
contactlist,accesstoallsharedpasswordsisautomaticallyrevoked.
FinallywepreventtheUserViewfromreturningthecurrentuser.ThisviewonlysupportGET
preventingusersfrombrowsingallmembers.
OnelastthingweneedbeforewecanhookthingsuponthefrontendistoupdatethePasswordFormsoit
allowsuserstosharetheirpasswordswithusersintheircontactlist.Update`apps/passwords/forms.py`
withthefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

fromdjango.formsimportModelForm
fromdjango.formsimportwidgets
fromdjango.forms.modelsimportModelMultipleChoiceField
fromdjango.utils.translationimportugettext_lazyas_

frommodelsimportPassword,PasswordContact

classPasswordForm(ModelForm):
classMeta:
model=Password
widgets={
'shares':widgets.CheckboxSelectMultiple
}

def__init__(self,user,*args,**kwargs):
super(PasswordForm,self).__init__(*args,**kwargs)
remove_message=unicode(_('Holddown"Control",or"Comm

forfieldinself.fields:
ifremove_messageinself.fields[field].help_text:
self.fields[field].help_text=self.fields[field]

#restrictthechoiceofuserstosharepasswordswithto
#user'sPasswordContacts
self.fields['shares']=ModelMultipleChoiceField(

http://10kblogger.wordpress.com/

10/43

12/6/2014

26
27
28

London Python developer/AWS devop engineer

queryset=PasswordContact.objects.filter(from_user=
.order_by('to_user__first_name'),
widget=widgets.CheckboxSelectMultiple())

Backbone.js
Letscreateaseparateapplicationforhandlingcontacts,althoughwellloaditallonthesamepage.
First,update`templates/passwords/password_list.html`asfollows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

{%extends"base.html"%}

{%loadsekizai_tags%}
{%loadbootstrap_toolkit%}

{%blockcontent%}
{%addtoblock"js"%}
<scripttype="text/javascript"src="{{STATIC_URL}}bootstrap/js

<!backbone><scripttype="text/javascript"src="{{STAT
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/back
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/back
<scripttype="text/javascript"src="{{STATIC_URL}}/js/contacts
{%endaddtoblock%}</pre>

&nbsp

<ulclass="navnavtabs">
<ulclass="navnavtabs">
<liclass="active"><ahref="#passwordPanel"datatoggle="tab
</ul>
</ul>

&nbsp

<ulclass="navnavtabs">
<ulclass="navnavtabs">
<li><ahref="#contactPanel"datatoggle="tab">Contacts</a
</ul>
</ul>

&nbsp

<pre>

</pre>
<divclass="tabcontent">

<divid="passwordPanel"class="tabpaneactive">

<h1class="pageheader">Passwords</h1>

http://10kblogger.wordpress.com/

11/43

12/6/2014

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

London Python developer/AWS devop engineer

Moveyourmouseoverapasswordtorevealit.Youcanonlyedit

<tableclass="tabletablestriped">
<thead>
<tr>
<th>Title</th>
<th>Username</th>
<th>Password</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<tdcolspan="5"><buttonclass="btnbtnprimary"datatoggle="mod
</tr>
</tfoot>
</table>

<divid="passwordModal"class="modalhidefade">
<formid="passwordForm"method="post">

<divclass="modalheader"><buttonclass="close"datadismiss=

<h3>PasswordDetails</h3>
</div>

<divclass="modalbody">{{form|as_bootstrap}}{%csrf_token%}

<divclass="modalfooter"><aclass="btn"href="#"datadismiss
</form></div>
</div>

<divid="contactPanel"class="tabpane">

<h1class="pageheader">Managecontacts</h1>

<divclass="well">

<h3>Addnewcontacts</h3>

Tofindotheruserstosharepasswordswith,entertheirusernam

<formclass="formsearch"><inputid="userSearch"class="searchq

<h3>Mycontacts</h3>

Youcansharepasswordswiththefollowingusers.Ifyouwantto

<tableclass="tabletablestriped">
<thead>
<tr>
<th>Name</th>
<th>Username</th>
<th>Actions</th>
</tr>

http://10kblogger.wordpress.com/

12/43

12/6/2014

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

London Python developer/AWS devop engineer

</thead>
</table>
</div>
</div>
<pre>
{%loadverbatim%}

<!ICanHaztemplates>{%comment%}
Mustacheanddjangobothuse{{}}tagsfortemplates,sowe
acustomtemplatetagtooutputthemustachetemplateexactl
{%endcomment%}
{%verbatim%}<scriptid="passwordRowTpl"type="text/html"
<td>
<ahref="{{url}}"target="_blank">
{{title}}
</a></td>

<td>{{username}}</td>

<tdclass="password">{{maskedPassword}}</td>

<td>{{notes}}</td>

<td>
{{#is_owner}}
<ahref="#"class="edit"title="Editthisentry"><i
<ahref="#"class="destroy"title="Deletethisentry
{{/is_owner}}</td>

//]]></script>

<scriptid="contactRowTpl"type="text/html">//<![CDATA[
<td>
{{to_user.first_name}}{{to_user.last_name}}</td

<td>{{to_user.username}}</td>

<td>
<ahref="#"class="destroy"title="Deletethisconta

//]]></script>

<scriptid="shareOption"type="text/html">//<![CDATA[
<labelclass="checkbox">
<inputtype="checkbox"value="{{id}}"name="shares
{{to_user.first_name}}{{to_user.last_name}}({{
</label>

//]]></script>
{%endverbatim%}
{%endblock%}

WeveaddedafewnewtemplatesandhavecreatedanicetabbedinterfacecourtesyofTwitterBootstrap.
So,tocreateourcontactapplication,enterthefollowingintoanewfile,`staticfiles/js/contacts.js`:
http://10kblogger.wordpress.com/

13/43

12/6/2014

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

London Python developer/AWS devop engineer

//loadthefollowingusingJQuery'sdocumentreadyfunction
$(function(){

//Contactmodel
varContact=Backbone.Model.extend({
remove:function(options){
mergedOptions={wait:true}
$.extend(mergedOptions,options)
this.destroy(mergedOptions)
}
})

//setuptheviewforacontact
varContactView=Backbone.View.extend({
tagName:'tr',

events:{
"clicka.destroy":"remove"
},

remove:function(event){
event.stopImmediatePropagation()
event.preventDefault()
if(confirm("Areyousureyouwanttodeletethisco
{
varthat=this

this.model.remove({error:function(model,respon
if(response.status==403){
alert("Youdon'thavepermissionto
}
else{
alert("Unabletodeletethatdata"
}
},
success:function(){
//updatetheformoptionsalittleha
$('#passwordForm').find(':checkbox').rem
$('#passwordForm').find('.checkbox').rem

varshareOptions=newArray()

that.options.collection.each(function
shareOptions.push(ich.shareOption(da
})

$(shareOptions.join('')).insertAfter(
}
})
}
},

render:function(){
//templatewithICanHaz.js(ich)
$(this.el).html(ich.contactRowTpl(this.model.toJSON(
returnthis
}

http://10kblogger.wordpress.com/

14/43

12/6/2014

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

London Python developer/AWS devop engineer

})

//definethecollectionofcontacts
varContactCollection=Backbone.Collection.extend({
model:Contact,
url:'/api/1.0/passwordcontacts/',

//maintainorderingbyfirst_name
comparator:function(obj1,obj2){
returnobj1.get('to_user').first_name.localeCompare(
}
})

/**
*Managesthelistofcontacts.
*/
varContactListView=Backbone.View.extend({
tagName:'tbody',

/**
*Constructor.Takesareferencetotheparentviewso
*methodsonit.
*/
initialize:function(options){
//instantiateapasswordcollection
this.collection=newContactCollection()

this.collection.bind('all',this.render,this)
this.collection.fetch()
},

addOne:function(contact){
this.$el.append(newContactView({model:contact,col
returnthis
},

addNew:function(data,options){
mergedOptions={wait:true}
$.extend(mergedOptions,options)

varcontact={
to_user:data.id
}

this.collection.create(contact,mergedOptions)
returnthis
},

render:function(){
this.$el.html('')
this.collection.each(this.addOne,this)
returnthis
}
})

/**
*Viewfortheoverallapplication.Weneedthisbecauseba

http://10kblogger.wordpress.com/

15/43

12/6/2014

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

London Python developer/AWS devop engineer

*bindeventsforchildrenof'el'.
*
*Inourtemplateourmodalisinside#app,sothisclassh
*interactionattheapplicationlevelratherthanstrictly
*collectionofPasswords(that'sthejobofthePasswordLi
*/
varContactPanelView=Backbone.View.extend({
el:'#contactPanel',
events:{
"click#contactPanel:submit":"handleSearch",
"keydown#contactPanel:input[type=text]":"handleSe
},

initialize:function(){
this.dataList=newContactListView({app:this})
},

displayError:function(model,response){
if(response.status==403){
alert("Youdon'thavepermissiontoeditthatda
}
else{
alert("Unabletocreateoreditthatdata.Pleas
}
},

render:function(){
this.$el.find('table').append(this.dataList.render()
},

handleSearch:function(event){
event.preventDefault()
event.stopImmediatePropagation()

varusername=$('#userSearch').val()

varthat=this

//performaGETrequesttotheuserSearchservicea
//returnsauser,createanewPasswordContact
$.ajax({
url:'/api/1.0/user/'+username,
dataType:'json',
success:function(data,textStatus,jqXHR){
that.dataList.addNew(data,{success:functio
$('#userSearch').val('')

//updatetheformoptions
$('#passwordForm').find(':checkbox').rem
$('#passwordForm').find('.checkbox').rem

varshareOptions=newArray()

that.dataList.collection.each(function
shareOptions.push(ich.shareOption(da
})

http://10kblogger.wordpress.com/

16/43

12/6/2014

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199

London Python developer/AWS devop engineer

$(shareOptions.join('')).insertAfter(
}})
},
error:function(jqXHR,textStatus,errorThrown)
if(jqXHR.status){
alert("Sorry,wecouldn'tfindthatuser
}
else{
alert("Therewasaproblemsearchingfor
}
}
})

returnthis
},

handleSearchOnEnter:function(event){
//processthemodaliftheuserpressedtheENTERk
if(event.keyCode==13)
{
returnthis.handleSearch(event)
}
}
})

varcontactPanel=newContactPanelView()
contactPanel.render()
})

Theaboveisquitesimilartopasswords.js,butgenerallysimpler.Abouttheonlycomplexcodeistodo
withupdatingthelistofcheckboxesonthepasswordformwhenusersaddordeletecontacts.Itsnotideal
havingselectorsintheviewlikethat,butwevemanagedtokeepittoaminimum,soIcanlivewithit
here.
The`handleSearch`methodusesthe`user`APIifitsuccessfullyreceivesaresponseitcreatesanew
PasswordContactandupdatestheuserscontactlist.

Tryitout
Thispostisabitlikeashoppinglist,buthopefullyitllhelpmaketherepository
(https://github.com/boosh/pwlocker)moreaccessible.Ifyouopentwodifferentbrowsersyoullbeableto
createtwodifferentusers,addthemtoeachotherscontactlistsandshareandrevokepasswordsbetween
them.

http://10kblogger.wordpress.com/

17/43

12/6/2014

London Python developer/AWS devop engineer

(http://10kblogger.files.wordpress.com/2012/05/final.png)
Wellfinishoffthisserieswithadiscussionofthecurrentarchitecture
(http://10kblogger.wordpress.com/2012/05/29/arestfulpasswordlockerwithdjangoandbackbonejs
part6/)andwhatcouldbedonetomaketheapplicationmoresecure.
COMMENTS1Comment
CATEGORIESProgramming

ARESTfulpasswordlockerwithDjangoand
backbone.jspart4
Sofarwevegotasingleuserpasswordstoringapplication
(http://10kblogger.wordpress.com/2012/05/26/arestfulpasswordlockerwithdjangoandbackbonejs
part3/).Thatsnotverysecureoruseful Sonowweregoingtosupportmultipleusersandlockdown
theappsousersmustbeauthenticated.WellalsolockdowntheRESTAPI.
TheresnowsufficientcodethatIllpointyouinthedirectionofcertainfilesintherepository
(https://github.com/boosh/pwlocker)forsomeofthemorestandardDjangocodeforthingssuchasuser
registration.

Supportingmultipleusers
Icreatedaregistrationformin`apps/users/forms.py`andupdated`urls.py`sothedjangoregistrationapp
woulduseit.Theregistrationformasksforusersfirstandlastnames,emailaddress,ausernameandtheir
password(twice).Thisisprettystandardstuff.

http://10kblogger.wordpress.com/

18/43

12/6/2014

London Python developer/AWS devop engineer

(http://10kblogger.files.wordpress.com/2012/05/registration.png)
Nowuserscanregister,weneedtoupdatethePasswordmodelin`apps/passwords/models.py`so
passwordsareassociatedwiththeuserthatcreatedthem.Addanewforeignkeyto
django.contrib.auth.models.User:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

fromdjango.dbimportmodels
fromdjango.contrib.auth.modelsimportUser

classPassword(models.Model):
"""
Representsausernameandpasswordtogetherwithseveralothe
"""
created_by=models.ForeignKey(User,related_name='+',editab
title=models.CharField(max_length=200)
username=models.CharField(max_length=200,
blank=True)
password=models.CharField(max_length=200)
url=models.URLField(max_length=500,
blank=True,
verbose_name='SiteURL')
notes=models.CharField(
max_length=500,
blank=True)
created_at=models.DateTimeField(auto_now_add=True,editable
updated_at=models.DateTimeField(auto_now=True,editable=

def__unicode__(self):
returnself.title

WelluseSouthtocreateamigrationwith`./manage.pyschemamigrationpasswordsauto`.Whenitasks
whattodoaboutdefaultsforthecreated_bycolumn,justmakeitsetthecolumnto1(oranynumberyou
like).Sinceweredeveloping,andwedonthavelegacydatatosupport,wecandontneedtomaintainthe
integrityofthedatabaseatthispoint.
Applythemigrationwith`./manage.pymigratepasswords`.

LockingdowntheRESTAPI
http://10kblogger.wordpress.com/

19/43

12/6/2014

London Python developer/AWS devop engineer

WeneedtodotwothingswiththeAPI:
1. Requireuserstobeauthenticatedtoaccessit,
2. Restrictdatauserscanworkwithtoonlythatwhichtheyhavecreated.
BecauseDjangoRESTAPIusesgenericclassbasedviews
(https://docs.djangoproject.com/en/1.4/topics/classbasedviews/),itsverysimpletoaddtheseconstraints.
Simplycreate`apps/api/views.py`andaddthefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

fromdjangorestframework.mixinsimportModelMixin
fromdjangorestframework.permissionsimportIsAuthenticated
fromdjangorestframework.viewsimportListOrCreateModelView,Inst

fromapps.passwords.resourcesimportPasswordResource

classRestrictToUserMixin(ModelMixin):
"""
Mixinthatrestrictsuserstoworkingwiththeirowndata
"""
defget_queryset(self):
"""
Onlyreturnobjectscreatedbythecurrentlyauthenticate
"""
returnself.resource.model.objects.filter(created_by=self

defget_instance_data(self,model,content,**kwargs):
"""
Setthecreated_byfieldtothecurrentlyauthenticatedu
"""
content['created_by']=self.user
returnsuper(RestrictToUserMixin,self).get_instance_data

classPasswordListView(RestrictToUserMixin,ListOrCreateModelView
"""
ListviewforPasswordobjects.
"""
resource=PasswordResource
permissions=(IsAuthenticated,)

classPasswordInstanceView(RestrictToUserMixin,InstanceModelView
"""
ViewforindividualPasswordinstances
"""
resource=PasswordResource
permissions=(IsAuthenticated,)

Wevecreatedamixintoaddthesamelogictobothclasses.ThemixinfiltersthequerysetusedbytheAPI
methodssothatthe`created_by`fieldisthecurrentlyauthenticateduser.Thispreventsusersfrom
accessingdatabelongingtootherusers.Italsosetsthe`created_by`fieldtothecurrentlyauthenticateduser
forCREATE(andUPDATE)operations.Itsverysimpleandveryelegant.
The`permissions`tupleinstructsDjangoRESTAPItorequirethatusersareauthenticatedinorderto
accessthoseresources.

http://10kblogger.wordpress.com/

20/43

12/6/2014

London Python developer/AWS devop engineer

Andnowupdate`apps/api/urls.py`tousetheseviewsinsteadoftheotherswehadconfigured:
1
2
3
4
5
6
7
8

fromdjango.conf.urls.defaultsimportpatterns,url

fromviewsimportPasswordListView,PasswordInstanceView

urlpatterns=patterns('',
url(r'^passwords/$',PasswordListView.as_view(),name='passwor
url(r'^passwords/(?P<id>[09]+)$',PasswordInstanceView.as_vie
)

IfyoucheckouttheAPIbrowserathttp://localhost:8000/api/1.0/passwords/
(http://localhost:8000/api/1.0/passwords/)youshouldnoticeyouneedtobeloggedintoaccessanything.
Onceyouregisterandlogin,youcanusetheAPI,butyoucanonlyviewdataassociatedwiththeaccount
youveloggedinas.However,ifyoutrytousethefrontendapplication,itwillloadthelistofpasswords,
butCREATE,UPDATEandDELETEAPIaccesswillberefused.Youcanseethisifyouhavefirebug
open,orifyoureloadthepage.ThisisbecauseDjangoisnowenforcingCSRFtokens.
Beforewefixthis,letsgooffonalittletangent.Unlessyouhadfirebugopen,youmaynothavespotted
thattheAPIwasrefusingyouraccesstheappappearedtoworkcorrectly.Soletsadderrorhandlingto
thebackbone.jsapplicationsoitllbeeasierforustoknowwhenwevefixedthisissue.
Tobealertedwhendeletionsfail,update`staticfiles/js/passwords.js`asfollows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

varPassword=Backbone.Model.extend({
...
remove:function(options){
mergedOptions={wait:true}
$.extend(mergedOptions,options)
this.destroy(mergedOptions)
},
...
})

varPasswordView=Backbone.View.extend({
...
remove:function(event){
event.stopImmediatePropagation()
event.preventDefault()
if(confirm("Areyousureyouwanttodeletethisentry?
{
this.model.remove({error:function(model,response)
if(response.status==403){
alert("Youdon'thavepermissiontodele
}
else{
alert("Unabletodeletethatdata")
}
}
})
}
},
...
})

http://10kblogger.wordpress.com/

21/43

12/6/2014

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

London Python developer/AWS devop engineer

varPasswordListView=Backbone.View.extend({
...
addNew:function(password,options){
mergedOptions={wait:true}
$.extend(mergedOptions,options)
this.passwords.create(password,mergedOptions)
returnthis
},

updatePassword:function(passwordData,options){
options=options||{}
varpassword=this.passwords.get(passwordData.id)
if(_.isObject(password))
{
//iteratethroughallthedatainpasswordData,set
//tothepasswordmodel
for(varkeyinpasswordData)
{
//ignoretheIDattribute
if(key!='id')
{
password.set(key,passwordData[key])
}
}

//persistthechange
password.save({},options)
this.passwords.sort()
}
},
...
})

varAppView=Backbone.View.extend({
...
displayError:function(model,response){
if(response.status==403){
alert("Youdon'thavepermissiontoeditthatdata")
}
else{
alert("Unabletocreateoreditthatdata.Pleasema
}
},

handleModal:function(event){
event.preventDefault()
event.stopImmediatePropagation()
varform=$('#passwordForm')

varpasswordData={
title:$(form).find('#id_title').val(),
username:$(form).find('#id_username').val(),
password:$(form).find('#id_password').val(),
url:$(form).find('#id_url').val(),
notes:$(form).find('#id_notes').val()
}

http://10kblogger.wordpress.com/

22/43

12/6/2014

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

London Python developer/AWS devop engineer

if($('#passwordModal').data('passwordId'))
{
passwordData.id=$('#passwordModal').data('password
this.passwordList.updatePassword(passwordData,{err
}
else
{
//addorupdatethepassword
this.passwordList.addNew(passwordData,{error:this
}

//hidethemodal
$('#passwordModal').modal('hide')

returnthis
},
...
})

WerepassinganerrorhandlerthroughthecodetotheCRUDmethods.Thehandlerchecksthestatus
codeanddisplaysanappropriatemessage.Nowwhenwetrytousethejavascriptapp,weatleasthave
somefeedbackthatthingsarefailing.
Wevealsomadebackbone.jswaituntilitreceivesaresponsefromtheserverbeforefiringachange
eventandupdatingtheUI(withthe`{wait:true}`option),sotheUIwillonlyupdateonsuccess.
Finally,tofixthisCSRFissue,weneedtoaddthe`{%csrf_token%}`templatetagto
`templates/passwords/password_list.html`.Justadditafterthe`{{form}}`tag.Thisaddstheactualtoken
tothetemplate.
Thenweneedtotweak`staticfiles/js/passwords.js`sojQuerywillsendthetokenasaheaderwitheach
AJAXrequest.
Addthefollowingattheendofthejavascriptcode,justinsidethefinalclosingbracesofthe`$(function()
{})`function:
1
2
3
4
5

//Setup$.ajaxtoalwayssendanXCSRFTokenheader:
varcsrfToken=$('input[name=csrfmiddlewaretoken]').val()
$(document).ajaxSend(function(e,xhr,settings){
xhr.setRequestHeader('XCSRFToken',csrfToken)
})

Now,trycreatingseveraldifferentusers,create,editanddeletesomedata,andtrytoaccessdataownedby
otherusers.YoushouldfindthattheAPIcorrectlyrestrictsyouraccesstodata,andthatthefrontend
javascriptappworkscorrectlytoo.

Maskingpasswords
http://10kblogger.wordpress.com/

23/43

12/6/2014

London Python developer/AWS devop engineer

Foraddedsecurity,wellmaskthepasswordsintheUI,andonlyrevealthemwhentheusermovestheir
mouseoverthem.Thisstopspeoplebeingabletoseeyourcompletelistofcredentialsiftheycanseeyour
screen.
First,wellupdatethebackbone.jsappin`staticfiles/js/passwords.js`tosetanewpropertyonthemodel
calledmaskedPasswordwhichwilljustbeastringofasterisks.Wellalsoaddsomeeventsfordisplaying
andhidingtheclearpasswords:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

varPassword=Backbone.Model.extend({
initialize:function(){
this.hidePassword()
},

//displaythepassword
showPassword:function(){
this.set({"maskedPassword":this.get('password')})
},

//hidethepassword
hidePassword:function(){
this.set({"maskedPassword":'********'})
},
...
})

varPasswordView=Backbone.View.extend({
...
events:{
"mouseover.password":"showPassword",
"mouseout.password":"hidePassword",
"clicka.edit":"editPassword",
"clicka.destroy":"remove"
},

showPassword:function(event){
event.stopImmediatePropagation()
this.model.showPassword()
},

hidePassword:function(event){
event.stopImmediatePropagation()
this.model.hidePassword()
},
...
})

NowupdatetheICanHaztemplatein`templates/passwords/password_list.html`topopulatethepassword
fieldusing`maskedPassword`insteadof`password`:
1
2
3
4
5
6
7

<scriptid="passwordRowTpl"type="text/html">
<td>
<ahref="{{url}}"target="_blank">
{{title}}
</a>
</td>
<td>{{username}}</td>

http://10kblogger.wordpress.com/

24/43

12/6/2014

8
9
10
11
12
13
14

London Python developer/AWS devop engineer

<tdclass="password">{{maskedPassword}}</td>
<td>{{notes}}</td>
<td>
<ahref="#"class="edit"title="Editthisentry"><iclass
<ahref="#"class="destroy"title="Deletethisentry"><
</td>
</script>

Reloadthefrontendappanditshouldloadthelistcorrectlywiththepasswordsmasked.Itshouldalso
displaytheclearpasswordwhenyouhoverthemouseovertheasterisks.However,CRUDoperationswill
failbecausebackbone.jswillsubmittheextra`maskedPassword`fieldtotheAPI,andDjangoREST
frameworkwillcomplain.
WevealreadygotawayofignoringcertainfieldssubmittedtotheAPI,sojustadd`maskedPassword`to
the`ignore_fields`tuplein`apps/passwords/resources.py`:
1
2
3
4

classPasswordResource(ModelResource):
...
ignore_fields=('created_at','updated_at','id','maskedPass
...

Summary
Weredoneforthisiteration.Theappnowsupportsmultipleusers,displayserrormessagestousersand
maskspasswords.
Inthepenultimatepartofthisseries(http://10kblogger.wordpress.com/2012/05/29/arestfulpassword
lockerwithdjangoandbackbonejspart5/),welladdtheabilitytosharepasswordsbetweenusers.
COMMENTS3Comments
CATEGORIESProgramming

ARESTfulpasswordlockerwithDjangoand
backbone.jspart3
WeleftourapplicationloadingdataviatheAPI(http://10kblogger.wordpress.com/2012/05/25/arestful
passwordlockerwithdjangoandbackbonejspart2/).NowweneedtosupportCRUDoperationsonit.
Tosupportdeletions,weneedtotweakonesettinginourDjangosettings.pyfileso.Bootstrapwill
performCRUDoperationsagainstaURLwithoutatrailingslash,butbydefault,Djangowilladdatrailing
slashtoanyURLswithoutone.Todisablethisbehaviour,set`APPEND_SLASH=False`insettings.py.
Alsoupdateyour`apps/api/urls.py`filetoremovethetrailingslash.Itshouldlooklikethis:
http://10kblogger.wordpress.com/

25/43

12/6/2014

1
2
3
4
5
6
7
8
9
10
11
12

London Python developer/AWS devop engineer

fromdjango.conf.urls.defaultsimportpatterns,url

fromdjangorestframework.viewsimportListOrCreateModelView,Inst
fromapps.passwords.resourcesimportPasswordResource

password_list=ListOrCreateModelView.as_view(resource=PasswordRe
password_instance=InstanceModelView.as_view(resource=PasswordRe

urlpatterns=patterns('',
url(r'^passwords/$',password_list,name='passwords_api_root'
url(r'^passwords/(?P[09]+)$',password_instance,name='passw
)

Weneedtoupdatethetemplatetoincludeanactionscolumntoletuserseditanddeleterows.Wellalso
addamodalusingTwitterbootstrapthatwillcontainaformtoletusersaddnewentriesorupdateexisting
ones.
Update`templates/passwords/password_list.html`asfollows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

{%extends"base.html"%}

{%blockcontent%}</pre>
<h1class="pageheader">Passwords</h1>
<divid="app">
<tableclass="tabletablestriped">
<thead>
<tr>
<th>Title</th>
<th>Username</th>
<th>Password</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<tdcolspan="5"><buttonclass="btnbtnprimary"datatoggle="moda
</tr>
</tfoot>
</table>
<divid="passwordModal"class="modalhidefade"><formid="passwor
<divclass="modalheader"><buttonclass="close"datadismiss="mod
<h3>PasswordDetails</h3>
</div>
<divclass="modalbody">{{form}}</div>
<divclass="modalfooter"><aclass="btn"href="#"datadismiss
<inputclass="btnbtnprimary"type="submit"value="Save"/></
</form></div>
</div>
<pre>
{%loadverbatim%}

<!ICanHaztemplates>
{%comment%}
Mustacheanddjangobothuse{{}}tagsfortemplates,sowen
acustomtemplatetagtooutputthemustachetemplateexactly

http://10kblogger.wordpress.com/

26/43

12/6/2014

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

London Python developer/AWS devop engineer

{%endcomment%}
{%verbatim%}
<scriptid="passwordRowTpl"type="text/html">//<![CDATA[
<td>
<ahref="{{url}}"target="_blank">
{{title}}
</a></td>

<td>{{username}}</td>

<tdclass="password">{{password}}</td>

<td>{{notes}}</td>

<td>
<ahref="#"class="edit"title="Editthisentry"><ic
<ahref="#"class="destroy"title="Deletethisentry"

//]]></script>
{%endverbatim%}
{%endblock%}

Sincewewanttoincludeaforminthetemplate,weneedtocreateaDjangoviewforthispagesowecan
includeit.
Edit`apps/passwords/url.py`asfollows:
1
2
3
4
5
6
7

fromdjango.conf.urls.defaultsimportpatterns,url

frommodelsimportPassword

urlpatterns=patterns('apps.passwords.views',
url(r'^$','password_list',name='password_list'),
)

Andcreateasimpleviewin`apps/passwords/view.py`:
1
2
3
4
5
6
7
8
9
10

fromdjango.shortcutsimportrender_to_response
fromdjango.templateimportRequestContext

fromformsimportPasswordForm

defpassword_list(request):
context=RequestContext(request)
form=PasswordForm()
context.update({'form':form})
returnrender_to_response('passwords/password_list.html',con

http://10kblogger.wordpress.com/

27/43

12/6/2014

London Python developer/AWS devop engineer

Nowweneedtocreatetheformjustastandardmodelformwilldo.Create`apps/passwords/forms.py`
andaddthefollowing:
1
2
3
4
5
6
7

fromdjango.formsimportModelForm

frommodelsimportPassword

classPasswordForm(ModelForm):
classMeta:
model=Password

Nowwecancreateourapplicationusingbackbone.js.Update`staticfiles/js/passwords.js`tocontainthe
following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

//loadthefollowingusingJQuery'sdocumentreadyfunction
$(function(){

//Passwordmodel
varPassword=Backbone.Model.extend({
remove:function(){
this.destroy()
},

validate:function(attrs){
if(attrs.title.length==0||attrs.password.length
{
return"Pleaseenteratitleandapassword"
}

if(attrs.url)
{
varre=/^(http[s]?:\/\/){0,1}(www\.){0,1}[azA
if(!re.test(attrs.url))
{
return"PleaseenteravalidURL"
}
}
}
})

//setuptheviewforapassword
varPasswordView=Backbone.View.extend({
tagName:'tr',

events:{
"clicka.edit":"editPassword",
"clicka.destroy":"remove"
},

editPassword:function(event){
event.preventDefault()
event.stopImmediatePropagation()
//callbackuptothemainapppassingthecurrent
//toallowausertoupdatethedetails
this.options.app.editPassword(this.model)
},

http://10kblogger.wordpress.com/

28/43

12/6/2014

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

London Python developer/AWS devop engineer

remove:function(event){
event.stopImmediatePropagation()
event.preventDefault()
if(confirm("Areyousureyouwanttodeletethisen
{
this.model.remove()
}
},

render:function(){
//templatewithICanHaz.js(ich)
$(this.el).html(ich.passwordRowTpl(this.model.toJSON
returnthis
}

})

//definethecollectionofpasswords
varPasswordCollection=Backbone.Collection.extend({
model:Password,
url:'/api/1.0/passwords/',

//maintainorderingbypasswordtitle
comparator:function(obj1,obj2){
returnobj1.get('title').localeCompare(obj2.get('
}
})

/**
*Managesthelistofpasswordsandrelateddata.Eventsar
*childnodesofthegeneratedelement.
*/
varPasswordListView=Backbone.View.extend({
tagName:'tbody',

/**
*Constructor.Takesareferencetotheparentviewso
*methodsonit.
*/
initialize:function(options){
//instantiateapasswordcollection
this.passwords=newPasswordCollection()

this.passwords.bind('all',this.render,this)
this.passwords.fetch()
},

addOne:function(password){
//passareferencetothemainapplicationintothe
//soitcancallmethodsonit
this.$el.append(newPasswordView({model:password,a
returnthis
},

addNew:function(password){
this.passwords.create(password)
returnthis

http://10kblogger.wordpress.com/

29/43

12/6/2014

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

London Python developer/AWS devop engineer

},

updatePassword:function(passwordData){
varpassword=this.passwords.get(passwordData.id)
if(_.isObject(password))
{
//iteratethroughallthedatainpasswordData,
//tothepasswordmodel
for(varkeyinpasswordData)
{
//ignoretheIDattribute
if(key!='id')
{
password.set(key,passwordData[key])
}
}

//persistthechange
password.save()
this.passwords.sort()
}
},

render:function(){
this.$el.html('')
this.passwords.each(this.addOne,this)
returnthis
}
})

/**
*Viewfortheoverallapplication.Weneedthisbecauseba
*bindeventsforchildrenof'el'.
*
*Inourtemplateourmodalisinside#app,sothisclassh
*interactionattheapplicationlevelratherthanstrictly
*collectionofPasswords(that'sthejobofthePasswordLi
*/
varAppView=Backbone.View.extend({
el:'#app',
events:{
"click#passwordForm:submit":"handleModal",
"keydown#passwordForm":"handleModalOnEnter",
"hidden#passwordModal":"prepareForm"
},

initialize:function(){
this.passwordList=newPasswordListView({app:this
},

render:function(){
this.$el.find('table').append(this.passwordList.rend
},

/**
*Allowsuserstoupdateanexistingpassword
*

http://10kblogger.wordpress.com/

30/43

12/6/2014

158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

London Python developer/AWS devop engineer

*@paramPasswordpassword:APasswordModelofthepas
*/
editPassword:function(password){
this.prepareForm(password.toJSON())
//storethepasswordIDasdataonthemodalitself
$('#passwordModal').data('passwordId',password.get(
$('#passwordModal').modal('show')
},

/**
*Setsupthepasswordform.
*
*@paramobjectpasswordData:Anobjectcontainingdata
*formvalues.Anyfieldsnotpresentwillbesettode
*/
prepareForm:function(passwordData){
passwordData=passwordData||{}

vardata={
'title':'',
'username':'',
'password':'',
'url':'',
'notes':''
}

$.extend(data,passwordData)

varform=$('#passwordForm')
$(form).find('#id_title').val(data.title)
$(form).find('#id_username').val(data.username)
$(form).find('#id_password').val(data.password)
$(form).find('#id_url').val(data.url)
$(form).find('#id_notes').val(data.notes)

//clearanypreviousreferencestopasswordIdinca
//clickedthecancelbutton
$('#passwordModal').data('passwordId','')
},

handleModal:function(event){
event.preventDefault()
event.stopImmediatePropagation()
varform=$('#passwordForm')

varpasswordData={
title:$(form).find('#id_title').val(),
username:$(form).find('#id_username').val(),
password:$(form).find('#id_password').val(),
url:$(form).find('#id_url').val(),
notes:$(form).find('#id_notes').val()
}

if($('#passwordModal').data('passwordId'))
{
passwordData.id=$('#passwordModal').data('pass
this.passwordList.updatePassword(passwordData)

http://10kblogger.wordpress.com/

31/43

12/6/2014

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239

London Python developer/AWS devop engineer

}
else
{
//addorupdatethepassword
this.passwordList.addNew(passwordData)
}

//hidethemodal
$('#passwordModal').modal('hide')

returnthis
},

handleModalOnEnter:function(event){
//processthemodaliftheuserpressedtheENTERk
if(event.keyCode==13)
{
returnthis.handleModal(event)
}
}
})

varapp=newAppView()
app.render()
})

Explanationofthebackbone.jscode
Weveaddedvalidationtothemodelalthoughwedontcurrentlydisplaytheerrormessagestotheuser.
Welladdressthisatafuturestage.
Wevealsoaddedamethodthatallowsustodeleteinstances.
ThePasswordViewlistenstoeventsfortheactionsweaddedtothepasswordtemplateandallows
objectstobeeditedanddeleted.
WeveaddedacomparatortothePasswordCollectionsoitstaysnicelyorderedbypasswordtitle.
ThePasswordListViewhandlesupdatestopasswordsaswellasaddingnewones.
Finally,theAppViewlistenstoeventsrelatedtosubmittingthepasswordformandresettingtheformwhen
themodalisclosed.
Whenpasswordsareedited,wekeeptrackofwhichpasswordisbeingeditedbysettingadataattributeon
themodalitselfwiththeIDofthemodeltoedit.Thisisntsetifweneedtoaddanewpassword.Every
timethemodalishidden,weresettheformandremovethisIDdataattributeincaseuserscancelthe
modal.

http://10kblogger.wordpress.com/

32/43

12/6/2014

London Python developer/AWS devop engineer

ThemodalisdisplayedandhiddenthankstoTwitterBootstrap
(http://twitter.github.com/bootstrap/javascript.html#modals)purelyduetoclassesanddataattributesinthe
HTML.

Onefinaltweak
Runtheserverwith`./manage.pyrunserver`andeverythingshouldworkexceptupdates.Bootstrap
submitsthecompletemodel,butinourDjangomodelwevedefinedseveralfieldsasuneditable.Sothe
finalthingweneedtodoistoedit`apps/passwords/resources.py`andmakeitdroptheuneditablefields
(created_at,updated_atandid)beforevalidatingtheform,otherwiseDjangoRESTframeworkwill
complainthatextrafieldshavebeensubmitted.Editthefilesoitcontainsthefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

fromdjangorestframework.resourcesimportModelResource
fromdjango.core.urlresolversimportreverse

frommodelsimportPassword

classPasswordResource(ModelResource):
model=Password
#bydefault,djangorestframeworkwon'treturntheIDbac
#needsitthough,sodon'texcludeit
exclude=None
ordering=('title',)
#djangorestframeworkwilloverwriteour'url'attributewi
#thatpointstotheresource,soweneedtoprovideanalter
include=('resource_url',)
ignore_fields=('created_at','updated_at','id')

defurl(self,instance):
"""
ReturntheinstanceURL.Ifwedon'tspecifythis,django
frameworkwillreturnageneratedURLtotheresource
"""
returninstance.url

defresource_url(self,instance):
"""
Analternativetothe'url'attributedjangorestframewo
addtothemodel.
"""
returnreverse('passwords_api_instance',
kwargs={'id':instance.id})

defvalidate_request(self,data,files=None):
"""
Backbone.jswillsubmitallfieldsinthemodelbacktou
somefieldsaresetasuneditableinourDjangomodel.So
toremovethoseextrafieldsbeforeperformingvalidation
"""
forkeyinself.ignore_fields:
ifkeyindata:

http://10kblogger.wordpress.com/

33/43

12/6/2014

40
41
42

London Python developer/AWS devop engineer

deldata[key]

returnsuper(PasswordResource,self).validate_request(dat

Editingdatanowworks:

(http://10kblogger.files.wordpress.com/2012/05/editing.png)

Summary
Atthispointourappsupportsthefollowing:
Whenweloadthepagehttp://localhost:8000/passwords/(http://localhost:8000/passwords/)backbone.js
loadsourpasswordsviatheAPIandrendersatablecontainingthedata.
WereabletoaddnewentrieswithAJAXviaourAPI.
Wecanupdateentries
Wecandeleteentries
Whatsnotsupported:
Wearevalidatingourbackbone.jsmodelontheclientsideandserverside,butifvalidationfailswe
dontinformtheuser.Weshouldprovidethemwiththisfeedback.
Theresnoauthenticationandpasswordsarentassociatedwithspecificusers.
Weneedtosupportsharingpasswordsbetweenuserssincethatsthepurposeofthisapp.
Itdbenicetobeabletoputpasswordsintocategoriesortotagthem.
Itdalsobenicetomaskpasswords,andonlyrevealthemwhenusershoveroverthem.
Userssavedpasswordsarestoredinplaintextinthedatabase.Thisisreallybad,butifweencrypt
them,howdowedecryptthemwhentheyresharedbetweenusers?Wellsolvethislater.
Inournextiteration(http://10kblogger.wordpress.com/2012/05/28/arestfulpasswordlockerwithdjango
andbackbonejspart4/)wellrelatepasswordswithusersandaddauthenticationtoourAPI.
COMMENTSLeaveaComment
CATEGORIESProgramming

http://10kblogger.wordpress.com/

34/43

12/6/2014

London Python developer/AWS devop engineer

ARESTfulpasswordlockerwithDjangoand
backbone.jspart2
Inthefirstpartofthisseries(http://10kblogger.wordpress.com/2012/05/25/arestfulpasswordlockerwith
djangoandbackbonejs/),wesetupDjango,createdabasicPasswordmodelandcreatedaRESTful
interfaceforit.Inthispost,weregoingtosetupbackbone.js
(http://documentcloud.github.com/backbone/)toloadourdatafromourAPI.

Getthecode
Dontforgetyoucanbrowseorclonethecodefromthegithubrepo(https://github.com/boosh/pwlocker).

Javascriptdependencies
Downloadthefollowingdependenciesandputthemintoasubdirectoryinsideadirectorymanagedby
Djangosstaticfilesapp.Iveused`contrib/backbone`.
backbonemin.js(http://documentcloud.github.com/backbone/backbonemin.js)corelibrary
underscoremin.js(http://documentcloud.github.com/underscore/underscoremin.js)backbone
dependency
json2.js(https://github.com/douglascrockford/JSONjs/blob/master/json2.js)backbonedependency
ICanHaz.min.js(http://icanhazjs.com/)fortemplating
IfyoudonthavejQueryorZeptoinstalled,youllneedoneofthosetoo.
Alsocreateafilecalled`contrib/js/passwords.js`thisiswherewellactuallycreateourbackbone
application.Addallofthesetoyourtemplate:
1
2
3
4
5
6
7
8
9

<scripttype="text/javascript"src="https://ajax.googleapis.com/aj

<!backbone>
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/backbo
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/backbo
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/backbo
<scripttype="text/javascript"src="{{STATIC_URL}}contrib/backbo

<scripttype="text/javascript"src="{{STATIC_URL}}/js/passwords.

http://10kblogger.wordpress.com/

35/43

12/6/2014

London Python developer/AWS devop engineer

Nowwerereadytostartbuildingthebackbonepartoftheapplication.Mygoalforthisstageisjusttoget
somethingupandrunningwhereitspullingdatainviatheAPI.
Openhttp://localhost:8000/passwords/(http://localhost:8000/passwords/)toviewwhatwevecurrentlygot
dataloadedviaDjango.Wewanttorecreatethis,butloadingdatausingbackbone.
Addthefollowingto`contrib/js/passwords.js`:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

//loadthefollowingusingJQuery'sdocumentreadyfunction
$(function(){

//Passwordmodel
varPassword=Backbone.Model.extend({})

//setuptheviewforapassword
varPasswordView=Backbone.View.extend({
render:function(){
//templatewithICanHaz.js(ich)
this.el=ich.passwordRowTpl(this.model.toJSON())
returnthis
}
})

//definethecollectionofpasswords
varPasswordCollection=Backbone.Collection.extend({
model:Password,
url:'/api/1.0/passwords/'
})

//mainapp
varAppView=Backbone.View.extend({
tagName:'tbody',

initialize:function(){
//instantiateapasswordcollection
this.passwords=newPasswordCollection()
this.passwords.bind('all',this.render,this)
this.passwords.fetch()
},

render:function(){
//templatewithICanHaz.js(ich)
this.passwords.each(function(password){
$(this.el).append(newPasswordView({model:passwo
},this)

returnthis
}
})

varapp=newAppView()
$('#app').append(app.render().el)
})

http://10kblogger.wordpress.com/

36/43

12/6/2014

London Python developer/AWS devop engineer

Theresnotmuchtoit.Backbone.jsrepresentsmodelsaskeyvalueobjectstowhichyoucanaddmethods.
Wedontneedanythingfancyatthisstage,sowejustextendthedefaultclass.Collectionscontainthe
logicforinteractingwithanendpointviaRESTwhichwevealreadysetup.Wevealsocreatedaview
thatcanrenderindividualpasswordobjects,andonethatrendersthecollectionbydelegatingtothe
passwordobjectview.
Onepointworthnotingintheabovecodeisthattherearenoselectorsinthebackboneclasses.Thismakes
themreusableandmorerobust.Theonlylinethatcontainsareferencetoaselectoristheverylastline
whichappendstheoutputoftherenderingofthewholeapplicationtoaspecificelement.
Thelastthingtodoistocreatesometemplates.WereusingICanHazwhichincludesMustache
(http://mustache.github.com/).Update`templates/passwords/password_list.html`soitcontainsthe
following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

{%extends"base.html"%}

{%blockcontent%}
<h1class="pageheader">Passwords</h1>
<tableclass="tabletablestriped"id="app">
<thead>
<tr>
<th>Title</th>
<th>Username</th>
<th>Password</th>
<th>Notes</th>
</tr>
</thead>
</table>

{%loadverbatim%}

<!ICanHaztemplates>
{%comment%}
Mustacheanddjangobothuse{{}}tagsfortemplates,sowen
acustomtemplatetagtooutputthemustachetemplateexactly
{%endcomment%}
{%verbatim%}
<scriptid="passwordRowTpl"type="text/html">
<tr>
<td>
<ahref="{{url}}"target="_blank">
{{title}}
</a>
</td>
<td>{{username}}</td>
<tdclass="password">{{password}}</td>
<td>{{notes}}</td>
</tr>
</script>
{%endverbatim%}
{%endblock%}

http://10kblogger.wordpress.com/

37/43

12/6/2014

London Python developer/AWS devop engineer

Saveeverythingandreloadthepageandyoushouldseeyourpageloadingasbefore.Ifyouvegotfirebug
installed,openit,switchtotheNettabandreloadthepagetomakesurethatthedataisbeingloaded
remotely.

Summary
NowwevegotareadonlyapplicationusingbackboneandDjango,itstimetomoveonandaddCRUD
support(http://10kblogger.wordpress.com/2012/05/26/arestfulpasswordlockerwithdjangoand
backbonejspart3/)totheapp.
COMMENTS1Comment
CATEGORIESProgramming

ARESTfulpasswordlockerwithDjango
andbackbone.js
InthisseriesImgoingtoshowyouhowtousebackbone.js(http://documentcloud.github.com/backbone/)
withDjango(https://www.djangoproject.com/).Weregoingtobecreatingapasswordlockerasitethat
willletyoukeeptrackofyourpasswordsandsharethemwithcolleagues.Mydevelopmentenvironmentis
Fedora16soshellscriptsareinbash.
Disclaimer:Thesitewillevolveandtobeginwithwillbeverynaivewithpasswordsstoredunencrypted
inthedatabase.Dontuseitinproduction
ToimplementaRESTfulinterfaceinDjango,welluseDjangoRESTframework(http://djangorest
framework.org/)whichmakescreatingRESTfulinterfacesfromDjangomodelssupereasy,andgivesyou
anicelittlebrowsersotheAPIsareselfdescribing.

Getthecode
Youcanbrowseorclonethesourcecodeforthisapplicationfromgithub
(https://github.com/boosh/pwlocker).ItsopensourcedundertheMITlicence.Ivealsoaddedtagsfor
mostofthepagessoyoucanfollowalongwiththetutorialifyouwish.

http://10kblogger.wordpress.com/

38/43

12/6/2014

London Python developer/AWS devop engineer

Setup
Letssetupthedjangoinstallationquickly.IclonedmystandardfoundationalDjangoprojectandtweaked
it.ItsjustabasicDjangopackagebutwithafewfabric(http://docs.fabfile.org/)scriptsthatmakebuilding
theprojecteasy.Italsoincludesuserregistrationandauthenticationwhichwellneedlateraswellas
Twitterbootstrap(http://twitter.github.com/bootstrap/).
Youcancheckoutmycode,orinstallDjangoyourself.Onceyouresetup,createapasswords
applicationinsideanappsdirectorywith`mkdirapps/passwords&&manage.pystartapppasswords
apps/passwords`Idothissocodeisbetternamespacedinsteadofhavingapplicationpackagesandother
packagesallmixedtogether:

Themodel
Tobeginwithwellcreateasimplemodelwithoutanyuserauthentication.Addthefollowingto
apps/passwords/models.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

fromdjango.dbimportmodels

classPassword(models.Model):
"""
Representsausernameandpasswordtogetherwithseveralothe
"""
title=models.CharField(max_length=200)
username=models.CharField(max_length=200,
blank=True)
password=models.CharField(max_length=200)
url=models.URLField(max_length=500,
blank=True,
verbose_name='SiteURL')
notes=models.TextField(
max_length=500,
blank=True,
help_text='Anyextranotes')
created_at=models.DateTimeField(auto_now_add=True,editable
updated_at=models.DateTimeField(auto_now=True,editable=

def__unicode__(self):
returnself.title

Run`./manage.pysyncdb`tocreateyourmodelinyourdatabase.WelluseSouth
(http://south.readthedocs.org/en/latest/)tomanagemigrations,soconvertyournewpasswordapptosouth
with`./manage.pyconvert_to_southpasswords`.
http://10kblogger.wordpress.com/

39/43

12/6/2014

London Python developer/AWS devop engineer

Nowwecanstarttheserverwith`./manage.pyrunserver`.
NowweneedtocreatearesourceforthismodelsoDjangoRESTframeworkknowshowtoserveitup.
Thisisprettysimplewhenusingabasicmodel.Createapps/passwords/resources.pyandaddthefollowing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

fromdjangorestframework.resourcesimportModelResource
fromdjango.core.urlresolversimportreverse
frommodelsimportPassword

classPasswordResource(ModelResource):
model=Password
#bydefault,djangorestframeworkwon'treturntheIDbac
#needsitthough,sodon'texcludeit
exclude=None
ordering=('created_at',)
#djangorestframeworkwilloverwriteour'url'attributewi
#thatpointstotheresource,soweneedtoprovideanalter
include=('resource_url',)

defurl(self,instance):
"""
ReturntheinstanceURL.Ifwedon'tspecifythis,django
frameworkwillreturnageneratedURLtotheresource
"""
returninstance.url

defresource_url(self,instance):
"""
Analternativetothe'url'attributedjangorestframewo
addtothemodel.
"""
returnreverse('passwords_api_instance',
kwargs={'id':instance.id})

Finally,weneedtosetupsomeURLs.TonamespacealltheAPIstogether,createanAPIappwith`mkdir
apps/api&&./manage.pystartappapiapps/api`andthenwireupDjangoRESTframeworkbyediting
apps/api/urls.py:
1
2
3
4
5
6
7
8
9
10
11
12

fromdjango.conf.urls.defaultsimportpatterns,url

fromdjangorestframework.viewsimportListOrCreateModelView,Inst
fromapps.passwords.resourcesimportPasswordResource

my_model_list=ListOrCreateModelView.as_view(resource=PasswordRe
my_model_instance=InstanceModelView.as_view(resource=PasswordRe

urlpatterns=patterns('',
url(r'^passwords/$',my_model_list,name='passwords_api_root'
url(r'^passwords/(?P<id>[09]+)/$',my_model_instance,name
)

Alsoaddthefollowingtoapps/passwords/urls.pytouseDjangosclassbasedgenericviewstocreatealist
viewofpasswords:
http://10kblogger.wordpress.com/

40/43

12/6/2014

1
2
3
4
5
6
7
8

London Python developer/AWS devop engineer

fromdjango.conf.urls.defaultsimportpatterns,url
fromdjango.views.genericimportListView

frommodelsimportPassword

urlpatterns=patterns('',
url(r'^$',ListView.as_view(model=Password),name='password_li
)

Finallyweneedtoincludethesetwourl.pyfilesintothemainurls.pyfileinyourprojectrootdirectory.
Addthefollowingtoitinsideyoururlpatterns:
1
2

url(r'^passwords/',include('apps.passwords.urls')),
url(r'^api/1.0/',include('apps.api.urls')),

Now,wevegotanicebrowserfortheAPIavailableathttp://localhost:8000/api/1.0/passwords/
(http://localhost:8000/api/1.0/passwords/).Openitupandcheckitout,andaddsomeentries.

(http://10kblogger.files.wordpress.com/2012/05/djangorestframeworkpassword
list_13379349076311.png)
Thatsreallycool.NotonlyhasitsavedustherepetitiveeffortofcreatingaRESTinterfaceourselves,but
wecangetonandpopulateitwithoutusinganybrowserextensions.Itsalsovalidatingourinputusinga
defaultmodelformforourmodel,soforexampleitsensuringthatinputinthesiteURLfieldvalidatesasa
URL.
Finally,beforewecanbrowsethelistofpasswordsweneedtocreateatemplatein
templates/passwords/password_list.htmlsothegenericviewcanrenderthelist:
1
2
3
4
5
6
7

{%extends"base.html"%}

{%blockcontent%}
<h1class="pageheader">Passwords</h1>
<tableclass="tabletablestriped">
<thead>
<tr>

http://10kblogger.wordpress.com/

41/43

12/6/2014

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

London Python developer/AWS devop engineer

<th>Title</th>
<th>Username</th>
<th>Password</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{%forpasswordinobject_list%}
<tr>
<td>{%ifpassword.site_url%}
<ahref="{{password.site_url}}"target="_blank"
{{password.title}}
</a>
{%else%}
{{password.title}}
{%endif%}
</td>
<td>{{password.username}}</td>
<td>{{password.password}}</td>
<td>{{password.notes}}</td>
</tr>
{%endfor%}
</tbody>
</table>
{%endblock%}

Now,youcanbrowseyourpasswordsathttp://localhost:8000/passwords/
(http://localhost:8000/passwords/)

(http://10kblogger.files.wordpress.com/2012/05/examplecom_1337934961899.png)
Nowwevegotsomedatainthedatabase,haveaRESTfulinterfacetoaccessitandhavealistof
passwordsonthefrontend,werereadytostartajaxingitwithbackbone.js.Thatsinpart2
(http://10kblogger.wordpress.com/2012/05/25/arestfulpasswordlockerwithdjangoandbackbonejs
part2/).
COMMENTS2Comments
CATEGORIESProgramming
LondonPythondeveloper/AWSdevopengineer
BlogatWordPress.com.TheBuenoTheme.
Follow

FollowLondonPythondeveloper/AWSdevopengineer
http://10kblogger.wordpress.com/

42/43

12/6/2014

London Python developer/AWS devop engineer

PoweredbyWordPress.com

http://10kblogger.wordpress.com/

43/43