Академический Документы
Профессиональный Документы
Культура Документы
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
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
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
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
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
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
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
"""
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
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
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
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>
 
<ulclass="navnavtabs">
<ulclass="navnavtabs">
<liclass="active"><ahref="#passwordPanel"datatoggle="tab
</ul>
</ul>
 
<ulclass="navnavtabs">
<ulclass="navnavtabs">
<li><ahref="#contactPanel"datatoggle="tab">Contacts</a
</ul>
</ul>
 
<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
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
</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
//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
})
//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
*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
$(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
(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
(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
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
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
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
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
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
<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
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
{%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
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
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
},
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
*@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
}
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
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
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
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
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
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
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
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
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
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
<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
PoweredbyWordPress.com
http://10kblogger.wordpress.com/
43/43