Hackyo’s Update — Week 12

Haryo Akbarianto Wibowo
Inspire Crawler
Published in
13 min readMay 15, 2016
Cola and Potato Chips :D (The picture isn’t owned by mine)

Introduction

Hai-hai ketemu dengan saya lagi, Hackyo pada minggu ke 13..

Tak terasa tinggal sebentar lagi waktu kita hampir berakhir.

Let’s cry T_T. — Haryo A W

What Have I Done?

Disini saya akan memberitahu apa saja yang sudah saya kerjakan seminggu ini dan apa yang akan saya tulis di sini:

  1. Kerjakan US terakhir ( Admin dapat menjalankan crawler melalui website
  2. Refactoring
  3. Analyse Design and Propose Alternative
  4. GitFlow
  5. Code Review
  6. Standard Code Convention
  7. Unit Testing
  8. Living Documentation

Kerjakan US Terakhir

Oke ini adalah US terakhir bagi saya. Tapi saya belum selesai mencobanya. Saat ini masih tahap mencoba-coba. Intinya disini adalah:

  1. Ada button di dashboard dimana button tersebut jika dipencet akan menjalankan crawlernya
  2. Ada button di dashboard dimana button tersebut jika dipencet akan menyetop crawlernya.
  3. Log akan berjalan terus dan akan update screen selamat x detik sekali.

Pertanyannya disini adalah how can we do it?

Berikut adalah caranya:

  1. Buat AJAX untuk dapat menerima respon dari button play. Lalu panggil request ke API playCrawler (POST)
  2. playCrawler akan melakukan scripting untuk menjalankan crawlernya.
  3. Buat AJAX untuk dapat menerima respon dari button stop. Lalu panggil request ke API stopCrawler (POST)
  4. stopCrawler akan melakukan scripting untuk menyetop crawlernya.

Oke, berikut adalah code yang saya buat untuk melakukan cara-cara tersebut.

Pada saat ini saya masih dalam tahap nyoba2, jadi programnya bukan InspireCrawler melainkan suatu program dimana didalamnya saya buat untuk tidak berhenti dan menulis sesuatu ke dalam file.

Pertama-tama saya tambahkan routes.php jadi seperti ini:

Route::post(‘admin/playCrawler’, function(Request $request){exec(‘bash -c “exec nohup setsid java test > /dev/null 2>&1 &”’);
return Response::json(“Hehe”);
});
Route::post(‘admin/stopCrawler’, function(Request $request){
$pidJava = exec(‘./tangina.sh’);
shell_exec(‘bash -c “kill ‘ . $pidJava . ‘”’);
return Response::json($pidJava);
});

Untuk tampilannya, tampilan dibuat oleh Puti Fitri Larasati. Jadi kalau mau lihat silahkan lihat ke storynya dia.

Oiya , supaya dia tidak berpindah halaman, harus menggunakan AJAX. Berikut adalah code yang saya tambahkan:

<script type=”text/javascript”>
$(document).ready(function(){
$(‘#playCrawler’).click(function(e){
$.ajaxSetup({
headers: {
‘X-CSRF-TOKEN’: $(‘meta[name=”csrf-token”]’).attr(‘content’)
}
})
var type = “POST”;
var formData = {
play: “Yeah”
};
var my_url = ‘http://localhost/timAul/web/public/admin/playCrawler';$.ajax({
type: type,
url: my_url,
data: formData,
dataType: ‘json’,
success: function (data) {
console.log(“BERHASILLL”);
},
error: function(data) {
console.log(‘Error!!!’)
}
});
});
$(‘#stopCrawler’).click(function(e){
$.ajaxSetup({
headers: {
‘X-CSRF-TOKEN’: $(‘meta[name=”csrf-token”]’).attr(‘content’)
}
})
var type = “POST”;
var formData = {
stop: “Yeah”
};
var my_url = ‘http://localhost/timAul/web/public/admin/stopCrawler';$.ajax({
type: type,
url: my_url,
data: formData,
dataType: ‘json’,
success: function (data) {
console.log(“BERHASILLL”);
},
error: function(data) {
console.log(‘Error!!!’)
}
});
});
});
</script>

stopCrawler dan playCrawler adalah class dari button yang saya buat.

Berikut tampilan tangina.sh

#!/bin/basha=”$(pgrep -f ‘java test’)”
echo $a

Dimana $a adalah PID dari program java saya. Saya coba playCrawler dan hasilnya seperti ini :

berhasilll

Dan ketika mememncet tombol stop:

berhasil

Yak berhasil. Hanya saja perlu dilakukan ke Inspire Crawler.

Hal-hal yang akan saya lakukan nantinya yaitu:

  1. Buat suatu API untuk update log
  2. Implementasikan ke Inspire Crawler.
  3. SECURITY : hanya admin yang bisa.

Error yang didapat :

  1. USER harus www-data kalau tanpa .sh

BUKTI : Github

Time : 5 jam (Cari-cari cara + ada error)

Refactoring

Decompose Conditional

Before :

//Avoid blank
quote = quote.trim();
author = author.trim();
if(!quote.isEmpty() && !author.isEmpty()) {
Quote quoteModel = new Quote(quote, author, sourceWebsite);
hasilFilter.add(quoteModel);
}

After

boolean isRequiredNotEmpty = !quote.isEmpty() && !author.isEmpty();
//Avoid blank
quote = quote.trim();
author = author.trim();
if(isRequiredNotEmpty) {
Quote quoteModel = new Quote(quote, author, sourceWebsite);
hasilFilter.add(quoteModel);
}

Encapsulate Field

Ada yg public access modifiernya, sediakan setter getternya:

Di Class TreeNode

public String value;
public TreeNode parent;
public ArrayList<TreeNode> childs = new ArrayList<TreeNode>();
public int level = -1;

Sesudahnya :

+ private String label;
+ private String value;
+ private TreeNode parent;
+ private ArrayList<TreeNode> childs = new ArrayList<>();
+ private int level = -1;

Ditambah setter getternya:

public String getLabel() {
return label;
}

public void setLabel(String label) {
this.label = label;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

public TreeNode getParent() {
return parent;
}

public void setParent(TreeNode parent) {
this.parent = parent;
}

public ArrayList<TreeNode> getChilds() {
return childs;
}

public void setChilds(ArrayList<TreeNode> childs) {
this.childs = childs;
}

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
}

Extract Class

You have one class doing work that should be done by two.

Before: (In Class Quote Filter)

method tagger and getListQuote in Quote Filter.

After :

Tagger in SentenceTagger

Extract Method

You have a code fragment that can be grouped together.

In Class LogCrawl:

Before :

pw.println(cal.getTime()); //2014/08/06 16:00:22
pw.println("Quotes : " + hasilCrawler.getQuote());
pw.println("Author : " + hasilCrawler.getAuthor());
pw.println("Source : " + hasilCrawler.getSource());

System.out.println(cal.getTime()); //2014/08/06 16:00:22
System.out.println("Quotes : " + hasilCrawler.getQuote());
System.out.println("Author : " + hasilCrawler.getAuthor());
System.out.println("Source : " + hasilCrawler.getSource());

System.out.println();

After:

/**
* Print ke file log_result
*
@param pw
* @param hasilCrawler
* @param cal
*/
private void printToTextLog(PrintWriter pw, Quote hasilCrawler, Calendar cal){
pw.println(cal.getTime()); //2014/08/06 16:00:22
pw.println("Quotes : " + hasilCrawler.getQuote());
pw.println("Author : " + hasilCrawler.getAuthor());
pw.println("Source : " + hasilCrawler.getSource());
}

/**
* Print ke standard output
*
@param hasilCrawler
* @param cal
*/
private void printToTerminalLog(Quote hasilCrawler, Calendar cal){
System.out.println(cal.getTime()); //2014/08/06 16:00:22
System.out.println("Quotes : " + hasilCrawler.getQuote());
System.out.println("Author : " + hasilCrawler.getAuthor());
System.out.println("Source : " + hasilCrawler.getSource());

System.out.println();
}

Dan

printToTextLog(pw,hasilCrawler, cal);
printToTerminalLog(hasilCrawler,cal);

Consolidate Duplicate Conditional Fragments

The same fragment of code is in all branches of a conditional expression.

Method add NER pada Class Sentence Tagger

String tagNer = word.get(CoreAnnotations.AnswerAnnotation.class);
if (tagNer.equalsIgnoreCase("PERSON"))
wordReturned += word.originalText() + '/' + tagNer; //Hanya untuk /PERSON
else
wordReturned += word.originalText();

Hide Method

A method is not used by any other class.

wordReturned += word.originalText();
if (tagNer.equalsIgnoreCase("PERSON"))
wordReturned += '/' + tagNer; //Hanya untuk /PERSON
public String getLeaves(TreeNode startNode){
String result = "";
Queue queue = new LinkedList<TreeNode>();
queue.add(startNode);

while(!queue.isEmpty()){
TreeNode thisNode = (TreeNode) queue.poll();
for (TreeNode node : thisNode.getChilds()) {
queue.add(node);
}
if(thisNode.getChilds().size() == 0){
result += thisNode.getValue() +" ";
}
}
return result;
}

to

wordReturned += word.originalText();
if (tagNer.equalsIgnoreCase("PERSON"))
wordReturned += '/' + tagNer; //Hanya untuk /PERSON
private String getLeaves(TreeNode startNode){
String result = "";
Queue queue = new LinkedList<TreeNode>();
queue.add(startNode);

while(!queue.isEmpty()){
TreeNode thisNode = (TreeNode) queue.poll();
for (TreeNode node : thisNode.getChilds()) {
queue.add(node);
}
if(thisNode.getChilds().size() == 0){
result += thisNode.getValue() +" ";
}
}
return result;
}

Remove Control Flag++

boolean yesVisit = false;

//cek apakah web yang akan dikunjungi ada di list daftar web dan merupakan halaman yang valid
for(int i=0; i<daftarWeb.size(); i++) {
yesVisit = !FILTERS.matcher(href).matches() && href.startsWith(daftarWeb.get(i));

if(yesVisit == true){
break;
}
}

return yesVisit;

Dijadikan:

//cek apakah web yang akan dikunjungi ada di list daftar web dan merupakan halaman yang valid
for(int i=0; i<daftarWeb.size(); i++) {
boolean yesVisit = !FILTERS.matcher(href).matches() && href.startsWith(daftarWeb.get(i));

if(yesVisit == true){
return yesVisit;
}
}

return false;

Rename Method

rename getLog dalam LogCrawl menjadi printLog

====

Analyse Design and Propose Alternative

Oke disini saya akan memberi alasan kenapa ALGORITMA yang saya pakai cocok untuk dipakai.

ada tiga yang saya code:

public List<Quote> getListQuote(String textDariWebsite, String sourceWebsite){

List<Quote> hasilFilter = new ArrayList<>();

//FOR unix file
//String pattern = "(\"\\{.*\\}\\\\NP .*\\{.*\\}\\\\VP\"|\\{.*\\}\\\\NP .*\\{.*\\}\\\\VP)(\\n(-|~|)(.*\\\\People)*|\\s*(-|~)(.*\\\\People)*)";

//FOR windows file
//String pattern2 = "(\".*\"|.*|)(\\n(-|—|~)(\\s([a-zA-Z]*\\/PERSON\\s*)*)|(-|—|~)(.*\\\\PERSON)+)";

//Regex For only NER
//String pattern3 = "(\".*\"|.*)(\\n(-|—|~|)\\s*(([a-zA-Z]*\\/PERSON\\s*)+)|(-|—|~)(.*\\\\PERSON)+)";

//Regex For only NER v2.0
String pattern4 = "(\"([a-zA-Z]+(\\.|,|;)*\\s*)*\"|.*)(\\n(-|—|~|)\\s*(([a-zA-Z]*\\/PERSON\\s *)+)|(-|—|~)(.*\\/PERSON)+)";

//Filter these out...
Pattern p = Pattern.compile(pattern4);

//REAL
String hasilTag = sentenceTagger.tagger(textDariWebsite);
// akan memberikan tag VP NP ke textDariWebsite
String result = sentenceTagger.partialIdentify(hasilTag);

Matcher m = p.matcher(hasilTag);

//TODO : make regex for NP VP

while
(m.find()){
String quote = m.group(1);
String author;
if(m.group(6) != null)
author = m.group(6);
else
author = m.group(9);
System.out.println(author);
quote = quote.replace("/PERSON","");
author = author.replace("/PERSON","");

boolean isRequiredNotEmpty = !quote.isEmpty() && !author.isEmpty();
//Avoid blank
quote = quote.trim();
author = author.trim();
if(isRequiredNotEmpty) {
Quote quoteModel = new Quote(quote, author, sourceWebsite);
hasilFilter.add(quoteModel);
}
}
// System.out.println(hasilFilter.size());
return hasilFilter;
}

Disini kenapa saya mencarinya dengan regex, bayangkan jika saya tidak menggunakan regex. Yaitu dengan split data perkalimat. Yang saya cari bisa saja ada bentuk seperti ini

ipsumipsumipsumipsumipsum ipsum ipsum. papa ssasa sasa sasa sa sa saasfjdsfjd safas .
- Alief Aziz

Maka jika ada sesuatu seperti diatas, maka tidak akan direcognize sebagai quote. Sedangkan dengan regex yang saya buat, hal diatas akan di recognize sebagai quote. Oke itu penyebab saya pake regex.

Untuk selanjutnya, saya menyimpan dengan String hasilTag = sentenceTagger.tagger(textDariWebsite);

Ini sebelum dilakukan pattern matching dengan regex. Saya menyimpan dengsn String bukan list karena pattern matching hanya bisa jika yang di match adalah string

Setelah itu, dilakukan pengecekan pada quote untuk dilihat jika ada tag , maka akan dihapus. Inipun dilakukan dengan pattern matching. Kenapa tidak pake .split. Sebenarnya sama saja, tetapi untuk memudahkan, saya memakai regex saaja (.replace)

2. Method addNer

/**
* Method ini akan mengembalikan tag PEOPLE untuk kata yang berupa nama orang
*
@param sentences beberapa kalimat yang ingin ditag
*
@return kata sudah tertag
*/
public String addNer(String sentences){
List<List<CoreLabel>> out = classifier.classify(sentences);
String wordReturned = "";
final Pattern FILTERS = Pattern.compile("(}|\\\\|n't|'re|’re|n’t|’m|’s|'s|’ve|’ll|'ll|'ve|'m|VP|NP|S|\\.|,)");

for (List<CoreLabel> sentence : out){
for(int i = 0; i < sentence.size(); i++) {
CoreLabel word = sentence.get(i);

String tagNer = word.get(CoreAnnotations.AnswerAnnotation.class);
wordReturned += word.originalText();
if (tagNer.equalsIgnoreCase("PERSON"))
wordReturned += '/' + tagNer; //Hanya untuk /PERSON


//Klo berikutnya bukan {, \\ n't, 'm pake spasi
if(i < sentence.size()-1) {
String nextWord = sentence.get(i + 1).originalText();
if (!FILTERS.matcher(nextWord).matches() && !word.originalText().equals("{")) {
wordReturned += " ";
}
}
}
wordReturned += "\n";
}
return wordReturned;
}

Kenapa saya pake list. Dari sananya memang sudah disediakan harus pakai list. Lalu kenapa saya tidak ubah ke yang lain? Karena yang paling cocok memang list. Sebenarnya pakai Queue juga bisa, namun kompleksitasnya sama dengan memakai list. Jadi yang cocok adalah List.

Disini dia akan ngeloop per kata untuk dicari apakah dia nama orang atau bukan. Kenapa harus loop satu-satu algoritmanya? Karena kita mengecek SEMUA kata-katanya. Kita tidak perlu melakukan apa-apa lagi.

Selain itu ada method ini didalam QuoteController:

public function testing(Request $request){
$sortSearch = $request->sort;
$sortBySearch = $request->sortby;
echo ‘this is Author = ‘ . $request->author;
echo ‘This is Quote = ‘ . $request->quote;
echo ‘This is Source = ‘ . $request->source;
echo ‘This is sort = ‘ . $request->sort;
echo ‘This is sortby = ‘ . $request->sortby . ‘\n’;
$allRequest = $request->only([‘author’, ‘quote’, ‘source’] );
$ternyataKosong = TRUE;
$authorThere = FALSE;
$author = “”;
$quoteThere = FALSE;
$quote = “”;
$sourceThere = FALSE;
$source = “”;
$whereQuery = [];
if ($request->has(‘author’)) {
$authorThere = TRUE;
$ternyataKosong = FALSE;
$author = $request->author;
//$whereQuery[‘author’] = $request->author;
}
if ($request->has(‘quote’)) {
$ternyataKosong = FALSE;
$quoteThere = TRUE;
$quote = $request->quote;
// $whereQuery[‘quote’] = $request->quote;
}
if ($request->has(‘source’)) {
$ternyataKosong = FALSE;
$sourceThere = TRUE;
$source = $request->source;
echo “MASOOOOK DONNG”;
// $whereQuery[‘source’] = $request->source;
}
$quotes = [];
if($ternyataKosong){
echo “masuk ga aaasa ini?”;
$quotes = Quotes::orderBy(“$sortSearch”,”$sortBySearch”)->paginate(15);
}
else{
if($authorThere){
echo “masuk ga ini?”;
$quotes = Quotes::where(‘author’, ‘like’, “%$author%”);
if($quoteThere){
$quotes = $quotes->where(‘quote’, ‘like’, “%$quote%”);
}
if($sourceThere){
$quotes = $quotes->where(‘source’, ‘like’ , “$source%”);
}
}

else if ($quoteThere) {
echo “masuk quote”;
$quotes = Quotes::where(‘quote’, ‘like’, “%$quote%”);
if($sourceThere){
$quotes = $quotes->where(‘source’, ‘like’ , “$source%”);
}
}
else {
echo “masa ini ga masuk?”;
$quotes = Quotes::where(‘source’, ‘like’ , “$source%”);
}
$quotes->orderBy(“$sortSearch”,”$sortBySearch”);
$quotes = $quotes->paginate(15);
}
$quotes->appends(\Input::except(‘page’))->links();

return view(‘admin.quote’,[
‘quotes’ => $quotes
]);

}

Knp saya mengecek semuanya saat awal-awal. Karena keterbatasan cara didalamnya, jadi saya memakan boolean untuk memastikan bahwa author diisi, source diisir ,dan quote diisi.

Jika Author tidak diisi, maka cek source atau quote. setelah itu tinggal conditional saja pakai if. (Ini diperlukan karena Quotes untuk dapat melakukan pengambilan data berdasarkan “where” harus memakai ::where. Jadinya harus ada yang jadi rootnya untuk jadi ::where. (maaf kalimatnya berantakan).

Itu menurut saya yang paling pas buat algoritma ini.

GitFlow

Disini saya mengingatkan bahwa kami sudah melaksanakan gitflow yang waktu itu kami canangkan.

Jadi gitflow kami yaitu:

Untuk awal-awal, kami akan mengerjakan di branch masing-masing.

Setelah itu kami akan menggabungaknnya ke dalam development. Dimana di development ini akan dilakukan testing dan dokumentasi.

Setelah semua OK, masuk kedalam master.

Code Review

Saya mereview code beberapa, yaitu:

Selain itu, saya akan mereview code alief untuk membuat halaman javadoc.

Selain itu pada InspireCrawler pada class TreeNode untuk method getChildrenOfLv2:

public String getChildrenOfLv2(){
String result = "";
Queue queue = new LinkedList<TreeNode>();
queue.add(this);
while(!queue.isEmpty()){
TreeNode thisNode = (TreeNode) queue.poll();
for (TreeNode node : thisNode.getChilds()) {
if(node.getLevel() ==2){
result += "{";
String nodeVal = node.getLeaves(node);

if(nodeVal.contains("/PERSON")){
nodeVal = nodeVal.replace("/PERSON", "");
result += nodeVal;
result += "}/PERSON ";
}else{
result += nodeVal;
result += "}/"+ node.getLabel() +" ";
}
}
queue.add(node);
}
if(thisNode.getLevel() >2){
return result;
}
}
return result;
}

Terlalu banyak line yang sia-sia. Seharusnya bisa digabung, pada part ini:

nodeVal = nodeVal.replace("/PERSON", "");
result += nodeVal;
result += "}/PERSON ";

dan

result += nodeVal;
result += "}/"+ node.getLabel() +" ";

Selain itu , harusnya class TreeNode membagi diri menjadi class Tree dan class Node, Jangan digabung. Karena hal ini membuat tidak high cohession.

Kami mengikuti kaidah ini untuk Java.

Beginning Comments

All source files should begin with a c-style comment that lists the programmer(s), the date, a

copyright notice, and also a brief description of the purpose of the program.

Sudah sesuai untk semuanya, untuk copyright, karena belum ada maka tidak dipakai. Contoh:

/**
* TreeNode is used to visualize the tree on the Parse Tree. It's used to find the 2nd constituent (NP,VP or other thing)
*
@author alief refactored by haryoaw
*
@version 19-04-2016
*/

Package and Import Statements

The first non-comment line of most Java source files is a

package statement. After that, import statements can follow.

Contoh: pada class TreeNode

package main;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

Class and Interface Declarations

Untuk dokumentasi menggunakan /** */

Untuk urutan public private protected, sudah, contoh class LogCrawl

These methods should be grouped by functional-

ity rather than by scope or accessibility. For

example, a private class method can be in

between two public instance methods. The goal is

to make reading and understanding the code eas-

ier (Sudah)

Wrapping Lines

//PADA QUOTE
if
(this.isManual == other.isManual
&& this.getAuthor().equals(other.getAuthor())
&& this.getID() == other.getID()
&& this.getSource().equals(other.getSource())
&& this.getQuote().equals(other.getQuote())){
return true;
}
//PADA TREENODE
public
String toString() {
return "label:" + this.getLabel()
+ ", value:"
+ this.getValue()
+", level:"
+ this.getLevel();
}

Dan banyak lagi yang sudah memenuhinya.

Number Per Line

private String label;
private String value;
private TreeNode parent;
private ArrayList<TreeNode> childs = new ArrayList<>();
private int level = -1;

Sudah memenuhi.

Placement

Sudah memenuhi untuk semua code di INspireCrawler bahwa variabel diinisiasi terlebih dahulu dalam scopenya lalu dilakukan eksekusi:

public String addNer(String sentences){
List<List<CoreLabel>> out = classifier.classify(sentences);
String wordReturned = "";
final Pattern FILTERS = Pattern.compile("(}|\\\\|n't|'re|’re|n’t|’m|’s|'s|’ve|’ll|'ll|'ve|'m|VP|NP|S|\\.|,)");

Class and Interface Declarations

Sudah.

if, if-else, if-else-if-else Statements

Ada beberapa code yang tidak pakai brace, lalu saya tambahkan

Saya mengubah if,if-else,dll sesuai dengan code convention

Naming Conventions

Program kami sudah memenuhi naming convention dari yang ditetapkan di dokumen tersebut.

  1. CLASS sudah sesuai
  2. METHOD sudah sesuai
  3. VARIABLE sudah sesuai
  4. CONSTANT sudah sesuai

Jadi semua sudah memenuhi Standard Code Convention (kecuali kalau ada yang lupa-lupa)

JUnit

Disini saya akan mengetest 2 method yang saya (addNer dan quoteFilter) buat dan method yang dibuat oleh ega pada ConfigReader dan shouldVisit yang dibuat oleh Puti.

QuoteFilter

Coverage :

1. QuoteQuoteQuoteQuoteQuote -Haryo/PERSON
2.

3. “QuoteQuoteQuoteQuoteQuote” -Haryo/PERSON
4. “QuoteQuoteQuoteQuoteQuote”
- Haryo/PERSON

5. “” — Haryo/PERSON
6. “Anu” /PERSON

7.

Quote Quote Quote Quote
- Haryo A W

Berikut adalah hasil screenshot saya:

Gagalllll
quote = quote.replace("\"","");

Bisa dilihat bahwa method ini gagal pada TC4, yaitu pada bagian ada new line. Hal ini perlu diperbaiki. (Belum saya perbaiki). untuk TC7 saya ketinggalan. Namun setelah di test, gagal juga.

ConfigReader

Ternyata class ini gagal membaca getPageToCrawl ke 6. Yang dibaca class ini page to crawlnya adalah 1 , padahal expectednya -1.

Coverage:

Karena fix jadi cuma ada satu kemungkinan.

Method addNer() pada class SentenceTagger

method ini berhasil mengidentifikasi apakah didalam ada person atau tidak.

Coverage :

  1. Sentence ada nama orang
  2. Sentence tidak ada nama orang

Method shouldVisit pada class InspireCrawler

Terlihat berhasil. Disini dia akan membedakan antara web yang bisa dikunjungi dan tidak dikunjungi. Url yang pertama adalah “http://quootelicious” yang ada di list config, sedangkan untuk yang TC ke dua URL “http://www.aliefkabur.com/” tidak ada di config. Disini terlihat berhasil lulus semua.

TIME SPENT : 7 Hours 45 Minutes.

Living Documentation

Pada sabtu kmarin, saya beserta kelompok saya melakukan penetapan living documentation, yaitu dengan menetapkan Definition of Done pada setiap fitur. Definition of Done ini bisa berubah seiring waktu tergantung dari client atau yang lainnya. Untuk bukti bisa dilihat di drive tentang Definition of Done.

Pengertian Living Documentation:

Living documentation is a dynamic method of system documentation that provides information that is current, accurate and easy to understand.

Time :

3 hours. (Pada saat ini saya membantu Kevin Ega Pratama untuk memecahkan masalahnya juga).

Conclusion

Sekian dari berita acara seminggu saya . Untuk kedepannya, saya akan mencoba Continuous Integration kalau sudah diberikan servernya oleh Kak Salman.

Terimakasih.

Everyone thinks of changing the world, but no one thinks of changing himself. — Leo Tolstoy

--

--

Haryo Akbarianto Wibowo
Inspire Crawler

Mad AI Enthusiast. I write mostly about Artificial Intelligence and Self Development. I also love to read Engineering, Psychology and Startup. Love to share!