Writing a Gallery App in Django, Part I
OnethingthatIseetheneedforonalmosteverysiteisaplacetoputimages.Whetherit’sabandwebsite,apersonalhomepage,oraschoolnewspaper,therewillbeaneedforaphotogallery.TheeasywaytodothatistouseanopensourcepackageavailablealreadylikeGallery2.Butthat’swritteninPHPanditwon’tintegrateeasilywiththerestofyoursite–especiallyifyouuseDjangoastheframeworkfortherestofyoursite.
Thesolution:writeagalleryapplicationforuseinyourwebsite.Atfirstitmayseemlikeadauntingtasktocreate,butasI’vefoundout,itcanbequiteeasy.Myimplementationisnotcompletelyupandrunningyet,butthat’sduetodesignissueswiththerestofthesite,notthephotogalleryappitself.Withnofurtheradieu,let’sdiveinandseehowthiscanwork.
class Album(models.Model): name = models.CharField(maxlength=128) slug = models.SlugField(prepopulate_from=("name",)) summary = models.TextField() date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True)
Asyoucansee,thisistheAlbumobjectwhichcontainsinformationaboutasetofassociatedphotos.Butwait,wedon’thavephotoscreatedyet!Let’sdothatnow.
class Photo(models.Model): title = models.CharField(maxlength=256) summary = models.TextField(blank=True, null=True) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) image = models.ImageField(upload_to='photos/%Y/%m') album = models.ForeignKey(Album) is_cover_photo = models.BooleanField()
Oksonowwehavealbumswhichhavephotos,andphotoshavealotofinformationlikeafilewhichcontainsanimage,atitle,andasummary.
Nowthatwehavetheseobjects,wehavesomechoicestomake.Theimagesneedtoberesizedintomediumandsmallimagesfordisplayonthelistanddetailpages,respectively.Thiscanbedoneseveralways:
UsetheHTMLwidthandheightattributestoresizetheimages.
CreateaDjangoviewwithwidthandheightparameterstobothresizeandservetheimages.(Lazycomputation)
Resizetheimagesonuploadandstorethemtodisk.(Upfrontcomputation)
Thefirstwayisnotoptimalfortworeasons.Firstlyeachpicturemustbedownloadedinit’sentirety.Thisisinefficientandcouldupsetpeoplewithlessbandwidthorbandwidthlimits.Secondly,whenmostbrowsersresizeimages,theydosousingpoorqualityfilters,resultinginalowqualityrepresentationofanimage.
Thesecondwayisthemostflexible,sincetheheightandthewidthcanbechangedinatemplateorinaviewandtheresizedimageswillchangeaccordingly.However,thereismoreon-the-flycomputationwiththisway,possiblyincreasingpageloadtimes.Also,Djangoisnotdesignedtobeusedtoservebinaryfilesdirectly,sotherecouldbeunforseenconsequenceswithhandlingalargenumberofphotosthisway.
Thethirdwayislessflexible,butithasonekeyadvantage:it’sfast.Sincethecomputationisdoneonupload,Apacheoranyotherhttpmediaservercanbeusedinstead,completelyremovingtheneedtousetheDjangoframeworkatall.Let’simplementitthisway.
First,we’llneedtooverloadthesavefunctionofthePhotomodel:
def save(self): if self.is_cover_photo: other_cover_photo = Photo.objects.filter(album=self.album).filter(is_cover_photo = True) for photo in other_cover_photo: photo.is_cover_photo = False photo.save() filename = self.get_image_filename() if not filename == '': img = Image.open(filename) img.thumbnail((512,512), Image.ANTIALIAS) img.save(self.get_medium_filename()) img.thumbnail((150,150), Image.ANTIALIAS) img.save(self.get_small_filename()) super(Photo, self).save()
BeforeItalkaboutthethumbnailingaspectofthisfunction,I’dliketobrieflyexplainwhat’sgoingonwiththecover_photoaspectofPhotoobjects.EachAlbumhasacoverphoto,soiftheAlbumneedstoberepresented,itcanberepresentedbyonespecialphoto.ThisisjustpartofthewaythatIhavedesignedmyobject,andcaneasilyberemoved.However,thereisasmallbitofobligatoryboilerplatecodeinthissavefunctionwhichsetsanyotheris_cover_photoattributestoFalseifnecessary.(Therecanonlybeonecoverphotoperalbum,afterall).I’llcomebacktodealingwithcoverphotoslater.
Firstoff,theifnotfilename==”statementisneededbecausesaveissometimescalledwithnoimagedata,andthatcanandwillthrowexceptionsifPILisusedonaNoneobject.Then,itresizesamedium-sized(512pxby512px)imageandsavesittoalocationprovidedbyget_medium_filename.Ileaveittoyoutodefineyourownget_medium_filenameandget_small_filenamewithyourownnamingconvention.IfollowedFlickr’sexampleofanunderscorefollowedbyanargument(image001.jpgbecomesimage001_m.jpgformediumandimage001_s.jpgforsmall).Finally,thismethodmustcalltheit’sparentsavemethodsothatalloftheotherattributesaresavedcorrectly.
Nowthatwe’veoverloadedthesavefunctionality,wearegoingtohaveaproblemwithdeletion.Djangowillautomaticallydeletetheoriginalimage,buttheresizedthumbnailswillbeleftonthediskforever.Thisisnotahugeproblem,butwedon’twantthattohappenanyways.Solet’stakecareofthatbyalsooverloadingthedeletefunctionofPhoto’smodel:
def delete(self): filename = self.get_image_filename() try: os.remove(self.get_medium_filename()) os.remove(self.get_small_filename()) except: pass super(Photo, self).delete()
Simplyput,itdeletesthethumbnailfilesandthencallsit’sparent’sdelete,whichwillinturndeleteit’soriginalfile.
Asidefromcreatingsomeoptionalhelperfunctionslikeget_small_image_urland/orget_medium_image_url,there’snotmuchmoretobedonewiththePhotomodel.Whatcanbedonestill,however,isinAlbum.WenowhavezerooronecoverphotosforeachAlbum,butit’sgoingtobetrickytoqueryforthiseachtime,solet’screateafunctioninAlbumtohelpusretrievetheassociatedcover_photoPhotoobject:
def get_cover_photo(self): if self.photo_set.filter(is_cover_photo=True).count() > 0: return self.photo_set.filter(is_cover_photo=True)[0] elif self.photo_set.all().count() > 0: return self.photo_set.all()[0] else: return None
Thatis,iftheAlbumhasaphotowithis_cover_photo==True,thengrabit,otherwisegrabthefirstimage.Iftherearenoimagesinthealbum,returnNone.That’sitforthemodels.Easy,huh?Justrunmanage.pysyncdb,andletDjangodotheheavyliftingforyou.
That’sallforpartoneofthisseriesonwritingagalleryapplicationwithDjango.Nextup:writingtheviews,urlconfs,andputtingitalltogether.Templatingwillbeleftuptoyou,sincetherearesomanywaystodisplaythisinformation,butsomeexampleswillbegiventopointyouintherightdirection.
Notetopurists:Iknowthatsomeofthefunctionalitythatisbeingimplementedashelperfunctionsinthemodelswouldbebetterimplementedascustomtemplatetags,butIfinditeasiertotakealessphilosophicalstanceonthe"right"waytodothingsandsometimesdowhat’smorepractical.Inthiscase,writingmodelfunctionsisamucheasiersolutionthancreatingcompletelynewtemplatetags.Ineithercase,movingtoanewsitewillrequirearewrite,soI’mnotevenconvincedthatithurtsreusability.