efactoring
Diterjemahkan Oleh : Wildan Maulana
Sebelumnya di Symfony
Selama hari ketiga, semua layer dari arsitektur MVC telah diperlihatkan dan dimodifikasi agar dapat menampilkan daftar pertanyaan-pertanyaan dengan baik di homepage. Aplikasi ini sudah terlihat makin baik, tetapi masih kurang isi.
Tujuan dari hari keempat adalah untuk menunjukkan list jawaban-jawaban sebuah pertanyaan, memberikan URL yang lebih bagus untuk halaman detail question, menambahkan class tambahan, dan memindahkan beberapa baris code ke tempat yang lebih baik. Ini akan cukup menolong Anda dalam memahami konsep template, modwlm routing policy (aturan routing), dan refractoring. Anda mungkin berpikir masih terlalu dini untuk menulis ulang kembali suatu code yang baru berumur beberapa hari, tapi kami akan menunjukkanya kepada Anda, dan melihat bagaimana perasaan Anda pada akhir tutorial ini.
Untuk membaca tutorial ini, Anda harus familiar dengan konsep-konsep MVC yang diimplementasikan di symfony. Akan menolong juga jika Anda memiliki sedikit gambaran tentang apa agile development itu.
Memperlihatkan Jawaban-Jawaban sebuah Pertanyaan
Pertama, mari kita lanjutkan adaptasi dari template yang digenerate oleh CRUD 'Question' selama hari kedua
Action question/show
digunakan untuk menampilkan detail dari sebuah
pertanyaan, yaitu jika Anda melawatkan parameter id
. Untuk mengetesnya,
panggil saja :
http://askeet/frontend_dev.php/question/show/id/1
Anda mungkin sudah mengenal halaman show
jika Anda telah bermain-main
dengan aplikasi ini sebelumnya. Disinilah kita akan menambahkan
jawaban-jawaban sebuah pertanyaan.
Sekilas mengenai action show
Pertama, mari kita lihat action show
, yang ada di file
askeet/apps/frontend/modules/question/actions/actions.class.php
:
public function executeShow() { $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id')); $this->forward404Unless($this->question); }
Jika Anda sudah familiar dengan Propel, Anda disini akan mengenal request
sederhana ke tabel Question
. Query ini bertujuan untuk mengambil record
yang unik yang memiliki nilai parameter id
sebagai primary key.
Pada URL yang diberikan pada contoh diatas, parameter id
akan memiliki
nilai 1
, sehingga methode retrieveByPk()
dari class QuestionPerr
akan
me-return object dengan tipe class Question
dengan `1`` sebagai primary key.
Jika Anda tidak familiar dengan Propel, kembalilah setelah Anda membaca beberapa dokumentasinya pada website mereka.
Hasil dari request ini akan dilewatkan ke template showSuccess.php
melalui
variabel $question
.
Method ->getRequestParameter('id'
milik object sfAction
kaan mendapatkan...
paremeter request yang bernama id
, baik dilewatkan dengan mode GET ataupun
POST.
Misalnya, jika Anda menulis URL seperti ini :
http://askeet/frontend_dev.php/question/show/id/1/myparam/myvalue
... Maka action show
akan dapat menerima myvalue
dengan merequest
$this->getRequestParameter('myparam')
.
Catatan: Method
forward4040Unless()
menirim ke browser halaman 404 jika pertanyaannya tidak ditemukan pada database. Merupakan kebiasaan yang bagus dalam mengahdapi kasus terfatal dan error yang dapat terjadi selama proses eksekusi dan symfony memberikan kepada Anda beberapa method sederhana untuk menolong Anda melakukan hal yang benar dengan mudah.
Memodifikasi template showSuccess.php
Template showSuccess.php
yang digenerate belum sesuai yang kita inginkan,
jadi kita akan menulis ulang total template ini. Bukalah
file frontend/modules/question/templates/showSuccess.php
dan gantilah contentnya dengan :
<?php use_helper('Date') ?> <div class="interested_block"> <div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo count($question->getInterests()) ?> </div> </div> <h2><?php echo $question->getTitle() ?></h2> <div class="question_body"> <?php echo $question->getBody() ?> </div> <div id="answers"> <?php foreach($question->getAnswers() as $answer): ?> <div> <?php echo count($answer->getRelevancys()) ?> points posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?> on <?php echo format_date($answer->getCreatedAt(), 'p') ?> <div> <?php echo $answer->getBody() ?> </div> </div> <?php endforeach ?> </div>
Anda disini telah mengenal div interested_block
yang telah ditambahkan pada
template interested_block
kemarin. Template ini hanya menampilkan
jumlah user yang tertarik pada sebuah pertanyaan.
Selain itu, markup yang digunakanpun hampir sama dengan yang ada pada list
,
kecuali tidak ada link_to
pada title nya.
Ini hanya menulis ulang code asal untuk menampilkan informasi tentang
sebuah pertanyaan saja.
Bagian yang baru adalah div answer
. div ini menampilkan semua jawaban
sebuah pertanyaan (dengan menggunakan method Propel yang sederhana
$question->getAnswers()
), dan untuk masing-masingnya, ditampilkan
relevancynya, nama pengarang, dan tanggal pembuatan, dan body tentu saja.
format_date()
adalah contoh lain dari template helpers dimana diperlukan
deklarasi awal. Anda dapat membaca lebih jauh mengenai helper ini dan
sintaksnya dan helper lainnya pada
Bab Internasionalisasi
dari buku symfony.(helper ini mempercepat kerjaan yang menyebalkan untuk
menampilkan tanggal dalam format yang enak untuk dilihat)
Catatan: Propel membuat nama method untuk melink tabel-tabel dengan manambahkan akhirsan '2' secara otomatis pada akhir nama tabel. Tolong, maafkan nama method yang jelek
->getRelevenacys()
karena method ini menghemat Anda beberapa baris code SQL.
Menambahkan beberapa data tes
Sekarang saatnya untuk menambahkan beberapa data untuk tabel answer
dan
relevancy
pada file data/fixtures/test_data.yml
(silahkan tambahkan
data lagi sesuai keinginan Anda):
Answer: a1_q1: question_id: q1 user_id: francois body: | You can try to read her poetry. Chicks love that kind of things. a2_q1: question_id: q1 user_id: fabien body: | Don't bring her to a donuts shop. Ever. Girls don't like to be seen eating with their fingers - although it's nice. a3_q1: question_id: q2 user_id: fabien body: | The answer is in the question: buy her a step, so she can get some exercise and be grateful for the weight she will lose. a4_q1: question_id: q3 user_id: fabien body: | Build it with symfony - and people will love it. Relevancy: rel1: answer_id: a1_q1 user_id: fabien score: 1 rel2: answer_id: a1_q1 user_id: francois score: -1
Reload data Anda dengan:
$ php batch/load_data.php
Bukalah action yang menunjukkan pertanyaan pertama untuk mengecek kalau modifikasi yang kita buat benar :
http://askeet/frontend_dev.php/question/show/id/XX
Catatan: Ganti XX dengan
id
dari pertanyaan pertama Anda.
Pertanyaan sekarang ditampilkan dengan tampilan yang lebih bagus, diikuti
Memodifikasi model, bagian I
Dapat dipastikan nama lengkap author akan diperlukan entah dimaan di
aplikasi kita. Anda juga bisa mempertimbangkan nama lengkap ini menjadi
attribute dari object User
.
Ini artinya ia harus menjadi sebuah method di model User
yang dapat
digunakan untuk mengambil nama lengkap, daripada menysunnya di sebuah
action. Mari kita tulis.
Bukalah askeet/lib/model/User.php
dan tambahkanlah method berikut :
public function __toString() { return $this->getFirstName().' '.$this->getLastName(); }
Mengapa method ini dinamakan __toString()
daripada getFullName()
atau
yang lainnya ? Karena method __toSrting()
adalah method default yang
digunakan PHP5 untuk meng-echo sebuah object.
Ini artinya Anda dapat mengganti baris
posted by <?php echo $answer->getUser()->getFirstName().' '.$answer->getUser()->getLastName() ?>
dari template askeet/apps/frontend/modules/question/templates/showSuccess.php
dengan :
posted by <?php echo $answer->getUser() ?>
untuk mencapai hasil yang sama. Mudah kan ?
Jangan melakukan hal yang sama berulang-ulang
Salah satu prinsip yang baik dari agile development adalah untuk menghindari
duplikasi code. Dikatakan "Don't Repeat Yourself" (D.R.Y.).
Ini dikarenakan adanya duplikasi code berarti dua kali lebih lama untuk
mereview, memodifikasi, mengeset dan memvalidasi daripada sebuah code yang
unik yang dienkapsulasi. Duplikasi code ini juga membuat proses maintaince sebuah
aplikasi lebi kompleks. Jika Anda memperhatikan pada bagia akhir daru
tutorial hari ini, Anda mungkin menemukan beberapa duplikasi code antara
template listSuccess
.php yanf ditulis kemarin dan showSuccess.php
:
<div class="interested_block"> <div class="interested_mark"> <?php echo count($question->getInterests()) ?> </div> </div>
Jadi sesio pertama Kita dari refactoring
akan menindahkkan beberapa baris code dari dua buah template dan menyimpannya
pada sebuah fragmeng, atau potongan code yang dapat digunakan kembali.
Buatlah file _interseted_user.php
pada direktori askeet/apps/frontend/modules/question/template/
dengan code berikut :
<div class="interested_mark"> <?php echo count($question->getInterests()) ?> </div>
Kemudian, ganti code awal pada kedua template tadi dengan:
<div class="interested_block"> <?php echo include_partial('interested_user', array('question' => $question)) ?> </div>
Sebuah fragment tidak memiliki akses langsung ke object-object yang ada.
Fragment menggunakan variabel $question
, sehingga ia harus didefinisikan
ketika memanggil include_partial
. Tambahan _
didepan nama file fragment
menolong dalam membedakan fragmen denan template yang sebenarnya yang ada
didirektori template. Jika Anda ingin belajar lebih jauh mengenai fragment,
bacalah Bab view
pada buku Symfony.
Memodifikasi model, bagian II
Panggilan $question->getInterest()
dari frgment yang baru melalukan sebuah
request ke database dan mereturn sebuah array object yang bertipe class
Question
. Ini adalah request yang cukup berat untuk sejumlah kecil orang
yg tertarik, dan ini mungkin saha akan membuat beban database teralu berat.
Ingat, panggilan ini juga dilakukan di template listSuccess.php
, tapi kali
ini dalam sebuah loop, untuk setiap pertanyaan pada list. Ide yang sangat
bagus untuk mengoptimalisasi masalah ini.
Salah satu solusi yang baik adalah membuat sebuah kolom pada tabel Question
yang bernama interested_users
, dan mengupdate kolom ini setiap kali
ketertarikan pada sebuah pertanyaan dibuat.
Peringatan: Kita akan mengubah model tanpa adanya cara yang jelas untuk mengetesnya, karena untuk saat ini belum ada cara untuk menambahkan record
Interest
melalui askeet. Jangan pernah sekali-kalli Anda memidifikasi sesuatu tanpa asa cara untuk mengetesnya.Tapi untuknya, kita memiliki cara untuk mengetes modifikasi ini, dan Anda akan menemukannya nanti, pada bagian ini.
Menambahkan sebuah field ke model object User
.
Sekarang, tanpa rasa takut, modifikasilaj model data
askeet/config/schema.xml
dengan menambahkan ke tabel ask_question
:
<column name="interested_users" type="integer" default="0" />
Tambahkan kolom yang sama ke tabel MySQL:
$ mysql -u youruser -p askeet -e "alter table ask_question add interested_users int default '0'"
Kemudian buat kembali modelnya:
$ symfony propel-build-model
Ya benar, Kita sudah membuat kembali model tanpa takut adanya perubahan pada
extension yang sudah kita buat pada model kita ! Hal ini karena, extension
pada class User
dibuat pada askeet/lib/model/User.php
, yang mewarisi
class-class yang digenerate oleh Propel di askeet/lib/model/om/BaseUser.php
.
Inilah alsannya mengapa Anda jangan pernag mengedit code pada direktori
askeet/lib/model/om/
: file-file yang ada disini ditulis ulang kembali
alias ditimpa setiap kali perintah build-model
dipanggil.
Symfony membantu memudahkan life cycle perubahan model pada tahap awal
proyek web apapun juga.
Catatan: Ada lebih dari satu cara untuk melakukannya
Jika Anda tidak ingin menulis statemen SQL, Anda dapat juga membuat ulang schema SQL dan mereload kembali test data:
$ symfony propel-build-sql $ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql $ php batch/load_data.php
Memodifikasi method save()
pada object Interest()
Mengupdate nilai dari filed baru ini harus dilakukan setiap kali
user menyatakan ketertarikannya pada sebuah pertanyaan, yaitu setiap kali
sebuah record ditambahkan ke tabel Interest
. Anda dapat mengimplementasikan
hal ini dengan trigger di MySQL, tapi hal ini merupakan solusi
yang bergantung pada sebuah database, dan Anda tidak akan dapat
pindah ke database lain dengan mudah.
Solusi terbaiknya adalah untuk memodifikasi model dengan cara
meng-override method save()
dari class Interest()
. Method ini
dipanggil setiap kali object dari class Interest
dibuat.
Jadi bukalah file askeet/lib/model/Interest.php
dan tulislah
method berikut :
public function save($con = null) { $ret = parent::save($con); // update interested_users in question table $question = $this->getQuestion(); $interested_users = $question->getInterestedUsers(); $question->setInterestedUsers($interested_users + 1); $question->save(); return $ret; }
Method save()
yang baru mengambil pertanyaan yang berhubungan dengan
interest yang sekarang, dan menaikkan nilai field interested_users
1.
Kemudian ia akan melakukan fungsi save()
seperti biasanya,
tapi karena $this->save();
akan mengakibatkkan infinite loop,
method ini memilih menggunakan parent:::save()
.
Mengamankan proses request update dengan transaction
Apa yang terjadi jika database gagal dalam proses update antara
object Question
dengan object Interest
?
Anda akan mendapatkan data yang corrupt.
Masalah ini sama dengan yang ditemui di bank, ketika sejumlam uang
ditransfer, maa request pertama adalah untuk mengurangi jumlah sebuah
account, dan request kedua untuk menambahkan jumlah di account yang lain.
Jika dua buah request sangat saling berketergantungan, Anda harus mengamankan eksekusinya dengan menggunakan transaction. Sebuah transaction adalah jaminan yang akan mensukseskan kedua kedua request, atau tidak sama sekali. Jika sesuatu ada yang salah pada salah satu request transaction, semua request yang berhasil sebelumnya akan dibatalkan, dan database akan kembali ke kondisi sebelum dimulainya transaction.
Method save()
kita ini adalah cara yang baik untuk mengilustrasikan
implementasi transaction di symfony.
Ganti code tadi dengan :
public function save($con = null) { $con = sfContext::getInstance()->getDatabaseConnection('propel'); try { $con->begin(); $ret = parent::save($con); // update interested_users in question table $question = $this->getQuestion(); $interested_users = $question->getInterestedUsers(); $question->setInterestedUsers($interested_users + 1); $question->save(); $con->commit(); return $ret; } catch (Exception $e) { $con->rollback(); throw $e; } }
Pertama, method akan membuka koneksi langsung ke database melalui
Creole. Antara deklarasi ->begin()
dan ->commit()
, transaction
memastikan semua request akan dilakukan atau tidak sama sekali.
Jika ada sesuatu yang gagal, sebuah exception akan terjadi,
dan database akan mengeksekusi sebuah rollback untuk kembali
ke kondisi sebelumnya.
Mengubah template.
Sekarang method ->getInterestedUsers()
dari object Question
telah bekerja dengan baik, sekarang saatnya untuk menyederhanakan
fragment _interested_user.php
dengan mengganti :
<?php echo count($question->getInterests()) ?>
dengan
<?php echo $question->getInterestedUsers() ?>
Dilihat dari segi jumlah request dan waktu eksekusi, cara ini seharusnya lebih baik.
Catatan: Terimakasi pada ide brilian kita untuk menggunakan fragment daripada menggunakan code yang duplikat pada template, modifikasi ini hanya perlu dilakukan sekali. Jika tidak, kita harus memodifikasi template
showSuccess.php
danlistSucces.php
, dan untuk orang yang malas seperti kita, ini mungkin terlalu berlebihan.
Mengetes benar tidaknya modifikasi yang kita lakukan
Kita akan mengecek memang benar-benar tidak ada yang salah dengan cara
merequest action show
lagi, tetapi sebelum itu, jalankan kembai
batc untuk import data yang telah Anda tulis kemarin :
$ cd /home/sfprojects/askeet/batch $ php load_data.php
Ketika membuat record untuk tabel Interesr
, object
sfPropelData
akan menggunakan method save()
yang telah
dioverride dan akan mengupdate record User
yang berhubungan.
Jadi ini adalah cara yang bagus untuk mengetes modifikasi dari model,
meskipun belum ada interface CRUD dengan object Interest
yang dibuat.
Perikasalah dengan merequest home page dan detail dari pertanyaan pertama:
http://askeet/frontend_dev.php/ http://askeet/frontend_dev.php/question/show/id/XX
Jumlah user yang tertaruk tidak berubah. Ini perubahan yang bagus!
Yang baru saja dilakukan untuk count($question->getInterests())
dapat juga dilakukan untuk count($answer->getRelevancys())
.
Sekarang Anda telah memahami bagaimana memodifikasi model,
Anda dapat latihan dalam hal ini dan melihat bagaimana kami
melakukannya pada repository SVN (lihatlah perubahan yang utama pada
schema.xml
), dan file test_data, class Relevancy
dan Answer
serta
template showSuccess
).
Routing
Sejak awal tutorial ini, kita menulis URL
http://askeet/frontend_dev.php/question/show/id/XX
Aturan routing default dari symfont memahami pola request seperti ini, seperti yang biasanya Anda request.
http://askeet/frontend_dev.php?module=question&action=show&id=XX
Tetapi dengan memiliki routing system telah membuka banyak kemungkinan. Kita dapat menggunakan judul pertanyaan sebagai URL, agar dapat membuka halaman yang sama dengan :
http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend
Cara ini dapat mengoptimalkan search engines mengindex halaman website, dan membuat URL lebih mudah dibaca.
Membuat versi alternatif dari judul
Pertama kita perlu mengkonversi judul menjadi versi yang dipisahkan dengan
tanda strip "-" untuk dijadikan sebagai URL.
Ada lebih dari satu cara untuk melakukan ini,
dan kita akan memilih untuk menyimpan judul alternatif ini sebagi kolom baru
dari tabel Question
. Pada schema.xml
, tambahkan baris berikut ke
tabel Question
:
<column name="stripped_title" type="longvarchar" />
...dan build kembali model
$ symfony propel-build-model
Anda juga perlu mengupdate database. Kali ini, kita akan menggenerate nya dan mengisi nya lagi dari awal:
$ symfony propel-build-sql $ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql
Kita segera akan mengoverride methos setTitle()
dari
object Question
sehingga ia mengeset judul yang
dipisah dengan tanda strip "-" sekaligus.
Custom class
Tetapi sebelum itu, Kita akan membuah sebuah custom class
untuk mengubah sebuah judul menjadi judul yang dipisah dengan
tanda strip "-", karena fungsi ini tidak benar-benar
berhubungan dengan object Question
(kita mungkin saja menggunakannya
untuk object Answer
).
Buatlah file myTools.class.php
dibawah direktori askeet/lib/
:
class myTools { public static function stripText($text) { $text = strtolower($text); // strip all non word chars $text = preg_replace('/\W/', ' ', $text); // replace all white space sections with a dash $text = preg_replace('/\ +/', '-', $text); // trim dashes $text = preg_replace('/\-$/', '', $text); $text = preg_replace('/^\-/', '', $text); return $text; } }
Sekarang buka class askeet/lib/model/Question.php
dan tambahkan L
public function setTitle($v) { parent::setTitle($v); $this->setStrippedTitle(myTools::stripText($v)); }
Perhatikan custom class myTools
tidak perlu ddeklarasikan:
symfony akan meloadnya secara otomatis jika diperlukan selama
ia diletakkan di direktori lib/
.
Kita dapat mereload kembali data kita:
$ php batch/load_data.php
Jika Anda ingin belajar lebih jauh mengenai custom class dan custom helpers, bacalah bab extension pada buku symfony.
Mengubah link ke action show
Pada template listSuccess.php
, ubahlah baris:
<h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>
dengan
<h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>
Sekarang buka action.class.php
dari module question
, dan ubahlah
action show
menjadi :
public function executeShow() { $c = new Criteria(); $c->add(QuestionPeer::STRIPPED_TITLE, $this->getRequestParameter('stripped_title')); $this->question = QuestionPeer::doSelectOne($c); $this->forward404Unless($this->question); }
Cobalah untuk menampilkan lagi list pertanyaan-pertanyaan dan mengaksesnya dengan meng-klik judul pertanyaan-pertanyaan ini:
http://askeet/frontend_dev.php/
URL menampilkan dengan benar judul sebuah pertanyaan:
http://askeet/frontend_dev.php/question/show/stripped-title/what-shall-i-do-tonight-with-my-girlfriend
Mengubah aturan routing
Tetapi tampilan ini tidak sesuai dengan yang kita inginkan.
Sekarang saatnya untuk mengesut aturan routing.
Bukalah file konfigurasi routing.yml
(yang terletak di direktori askeet/apps/frontend/config/
)
dan tambahkanlah aturan berikut pada bagian atas file:
question: url: /question/:stripped_title param: { module: question, action: show }
Pada baris url
, kata question
adalah custom text
yang akan muncul pada URL terakhir,
sementara stripped_title
adalah sebuah parameter
(ia dimulai dengan :
).
Baris ini membentuk sebuah pattern yang oleh system routing
symfony akan diterapkan pada semua link ke panggilan action
question/show
- karena semua link pada template kami menggunakan
helper link_to()
.
Sekarang saatnya untuk melakukan tes terakhir: Menampilkan homepage kembali, klik pada judul pertanyan pertama. Bukan saja pertanyaan pertama muncul(membuktikan kalau tidak ada yang rusak), tetapi address bar dari browser Anda sekarang menampilkan:
http://askeet/frontend_dev.php/question/what-shall-i-do-tonight-with-my-girlfriend
Jika Anda ingin belajar lebih banyak mengenai fitur routing, bacalah [bab aturan routing] (http://www.symfony-project.com/book/1_0/09-Links-and-the-Routing-System) pada buku symfony.
Sampai ketemu Besok
Hari ini, websitenya sendiri, tidak mendapatkan banyak fitur baru. Tetapi, Anda melihat lebih banyak coding template, Anda tahu bagaimana memodifikasi model, dan keseluruhan code telah direfractor pada banyal tempat.
Hal ini sering terjadi pada siklus hidup sebuah project symfony: code yang dapat digunakan kembali direfractor menjadi fragment atau sebuah class custom, code yang nampak pada sebuah action atau template dan yang sebenarnya milik model dipindahkan ke mode. Meskipun hal ini akan membuat code menjadi tersebar pada file-file kecil yang banyak pada berbagai direktori, proses maintain dan pengembangan project menjadi lebih mudah. Selain itu, struktur file symfony project yang sudah tetap akan memudahkan kita untuk mencari dimana sebuah code sebenarnya berada (helper, model, template, action, custom class, dll).
Refractoring yang telah dilakukan hari ini akan mempercepat proses development pada hari-hari yang akan datang, Dan kita akan melakukan refractoing kembali secara periodik pada project ini, karena dengan cara develop kita - membuat sebuah fitur tanpa khawatir tentang fungsionalitas yang akan dibuat nantinya - memerlukan struktur code yang bagus jika tidak kita akan berakhir dengan code yang acak-acakan.
Apa saja untuk besok ? Kita akan mulai menulis sebuah form dan melihat bagaimana mendapatkan informasi dari form tersebut. Kita juga akan memisahkan daftar pertanyaan dari home page menjadi beberapa halaman. Untuk sekarang, silahkan mendownload code sakarang dari repository SVN (dengan tag release_day_4) di :
http://svn.askeet.com/tags/release_day_4/
dan kirimlah pada kami pertanyaan apapun dengan menggunakan mailing-list askeet atau forum-nya.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.