Fakultas Ilmu Komputer UI

Commit 8e8b8d2d authored by Izzan Fakhril Islam's avatar Izzan Fakhril Islam
Browse files

Merge branch 'tutorial-4' into 'master'

Tutorial 5 PMPL

See merge request !7
parents 53548255 f56c9afa
Pipeline #24151 failed with stages
in 23 minutes and 11 seconds
......@@ -3,36 +3,29 @@ stages:
- deploy
- functional-test
before_script:
- apt-get install -f
- apt-get update -qy
- apt-get install -y python-dev python-pip sudo postgresql postgresql-client libpq-dev libxss1 libappindicator1 libindicator7
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
- echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
- apt-get update -qq && apt-get install -y -qq unzip
- apt-get install -y google-chrome-stable
- export CHROME_BIN=/usr/bin/google-chrome
- google-chrome --version
- apt-get install -y xvfb
- wget https://chromedriver.storage.googleapis.com/77.0.3865.40/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- service postgresql start
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE USER $DB_USERNAME WITH PASSWORD '$DB_PASSWORD' CREATEDB"
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USERNAME"
- pip install --upgrade pip
- pip install -r requirements.txt
- pip install --upgrade --ignore-installed urllib3
- python manage.py makemigrations
- python manage.py migrate
- python manage.py collectstatic --no-input
- python manage.py runserver 8000 &
UnitTest:
image: python:3.6
stage: unit-test
before_script:
- apt-get install -f
- apt-get update -qy
- apt-get install -y sudo postgresql postgresql-client libpq-dev libxss1 libappindicator1 libindicator7
- python --version
- apt-get install -y xvfb
- service postgresql start
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE USER $DB_USERNAME WITH PASSWORD '$DB_PASSWORD' CREATEDB"
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USERNAME"
- pip install --upgrade pip
- pip install -r requirements.txt
- pip install --upgrade --ignore-installed urllib3
- python manage.py makemigrations
- python manage.py migrate
- python manage.py collectstatic --no-input
- python manage.py runserver 8000 &
when: on_success
script:
- coverage run --include='tutorial_*/*' manage.py test -p "unit_test*.py"
- coverage report -m
- python manage.py test tutorial_5.unit_tests
- python manage.py test -p "unit_test*.py"
DeployToHeroku:
image: ruby:2.4
......@@ -49,8 +42,33 @@ DeployToHeroku:
url: $HEROKU_APP_HOST
FunctionalTest:
image: python:3.6
stage: functional-test
before_script:
- apt-get install -f
- apt-get update -qy
- apt-get install -y sudo postgresql postgresql-client libpq-dev libxss1 libappindicator1 libindicator7
- python --version
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
- echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
- apt-get update -qq && apt-get install -y -qq unzip
- apt-get install -y google-chrome-stable
- export CHROME_BIN=/usr/bin/google-chrome
- google-chrome --version
- apt-get install -y xvfb
- wget https://chromedriver.storage.googleapis.com/77.0.3865.40/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- service postgresql start
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE USER $DB_USERNAME WITH PASSWORD '$DB_PASSWORD' CREATEDB"
- sudo -u $DB_DEFAULT_OWNER psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USERNAME"
- pip install --upgrade pip
- pip install -r requirements.txt
- pip install --upgrade --ignore-installed urllib3
- python manage.py makemigrations
- python manage.py migrate
- python manage.py collectstatic --no-input
- python manage.py runserver 8000 &
when: on_success
script:
- coverage run --include='tutorial_*/*' manage.py test -p "functional_test*.py"
- coverage report -m
- python manage.py test tutorial_5.functional_tests
- python manage.py test -p "functional_test*.py"
......@@ -7,6 +7,8 @@
1. Tutorial 1: Web Page Pribadi berbasis *Python* dengan *Unit test*.
2. Tutorial 2: Web Page *Todo List* dengan *Unit test* dan *Functional test*.
3. Tutorial 3: *Test Isolation* pada Django
4. Tutorial 4: Prettification Test (*Functional test* pada CSS Django)
5. Tutorial 5: Test Organization
**URL Heroku:** https://pmpl-izzan.herokuapp.com/
......@@ -389,3 +391,186 @@ Dan berikut adalah konten dari `tutorial_2.html` saya.
```
## Penjelasan Tutorial 5
Berdasarkan buku **Test-Driven Development with Python 2nd Edition,** tutorial ini memiliki beberapa keterkaitan dengan materi yang disajikan di bab 12 (**Splitting Our Tests into Multiple Files, and a Generic Wait Helper**) dan bab 13 (**Validation at the Database Layer**), diantaranya adalah sebagai berikut.
1. Membuat direktori baru untuk pemecahan file-file *unit test* dan *functional test* dalam folder django app `tutorial_5` menjadi seperti dibawah ini.
- ```
.
└── tutorial_5
├── __init__.py
├── apps.py
├── functional_tests
| ├── __init__.py
| ├── base.py
| ├── test_css_aesthetics.py
| ├── test_todo_commentary.py
| ├── test_todo_item.py
| └── test_website_elements.py
└── unit_tests
├── __init__.py
├── test_todo_comment_table_models_django_testcase.py
└── test_todo_table_models_django_testcase.py
```
Pada tutorial 5, saya diminta untuk memisahkan beberapa *test case* dalam *unit test* dan *functional test* menjadi beberapa file yang terpisah, dan menerapkan *inheritance* pada modul *functional test* saya, dengan *parent* class nya ada di potongan file `base.py` seperti berikut.
```python
class FunctionalTest(StaticLiveServerTest):
def setUp(self):
chrome_options = Options()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-dev-shm-usage')
# chrome_options.add_argument('--disable-gpu')
self.selenium = webdriver.Chrome('./chromedriver', chrome_options=chrome_options)
self.cases = [
(5, "oh, tidak"),
(2, "sibuk tapi santai"),
(0, "waktunya berlibur"),
]
self.MAX_WAIT = 10
self.host = self.live_server_url + "/tutorial-2/"
self.selenium.implicitly_wait(3)
super(FunctionalTest, self).setUp()
def tearDown(self):
self.selenium.quit()
super(FunctionalTest, self).tearDown()
```
Selanjutnya, pada file-file yang lain saya meng-*extend* class `FunctionalTest` yang sudah dibuat, seperti contoh pada potongan file `test_todo_item.py` berikut.
```python
from .base import FunctionalTest
from selenium.webdriver.common.keys import Keys
class TodoItemTest(FunctionalTest):
def test_input_todo_item(self):
selenium_host = self.get_host_from_selenium(self.host)
todo_date = selenium_host.find_element_by_id('todo_date')
todo_date.send_keys('23052019')
todo_date.send_keys(Keys.TAB)
todo_date.send_keys('1245')
todo_text = selenium_host.find_element_by_id('activity')
todo_text.send_keys('Dummy')
todo_text.send_keys(Keys.ENTER)
self.wait_for_row_list_in_table("Dummy", "todo_table")
```
Hal yang sama juga berlaku untuk file-file dalam folder `unit_tests` yang dimana setiap *test case* dipisahkan menjadi beberapa file terpisah berdasarkan jenis *models* dan *forms* yang ingin dites.
2. Mengimplementasikan **generic explicit wait helper** dalam *functional test* dengan membuat method berikut.
```python
# Same as wait_for_row_list_in_table but using lambda expressions
def wait_for(self, fn):
start_time = time.time()
while True:
try:
return fn()
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > self.MAX_WAIT:
raise e
time.sleep(0.5)
```
Kemudian, method yang sudah dibuat digunakan dalam bentuk **lambda expressions** potongan kode berikut.
```python
def test_input_todo_commentary_item(self):
selenium_host = self.get_host_from_selenium(self.host)
todo_date = selenium_host.find_element_by_id('comment_date')
todo_date.send_keys('23122019')
todo_comment = selenium_host.find_element_by_id('comment')
todo_comment.send_keys("Dummy comment")
todo_comment.send_keys(Keys.ENTER)
self.wait_for(
lambda: self.assertIn("Dummy comment", selenium_host.page_source)
)
```
Salah satu keuntungan dalam pengimplementasian **generic explicit wait helper** adalah parameternya yang merupakan sebuah fungsi, bukan sebuah *primitive data type* sehingga dapat digunakan untuk berbagai macam variasi fungsi *waiter*.
3. Mengimplementasikan **database layer validation** pada *unit test* dengan membuat **model-layer validation** seperti pada potongan kode dibawah ini.
```python
# 1
def test_add_todo_item_via_model(self):
TodoList.objects.create(
date=timezone.now(),
todo_list=DUMMY_TODO_ITEM
)
todo_count = TodoList.objects.all().count()
self.assertEqual(todo_count, 1)
#2
def test_cannot_add_empty_todo_model(self):
new_todo = TodoList(
date="",
todo_list="",
)
with self.assertRaises(ValidationError):
new_todo.save()
new_todo.full_clean()
```
**Penjelasan:** Pada method nomor 1, dilakukan tes untuk memverifikasi apabila sebuah model memenuhi persyaratan dari properti-propertinya, dan dapat di-*save* dengan sukses, dengan melakukan *assertion* pada jumlah model `TodoList` yang berhasil dibuat. Sedangkan pada method nomor 2, dilakukan tes untuk mengecek apabila sebuah model tidak memenuhi persyaratan dari properti-propertinya, tidak dapat di-*save* dan akan melempar *exception* `ValidationError`
Selain mengimplementasikan dari *model-layer validation* saya juga membuat test untuk memvalidasi database dengan mengeceknya menggunakan `POST` request, seperti ditunjukkan pada potongan kode dibawah.
```python
# 1
def test_POST_request_create_todo(self):
self.client.post(
'/tutorial-2/add_todo/',
data={
'date': '2019-09-12T15:15',
'activity': DUMMY_TODO_ITEM
}
)
todo_count = TodoList.objects.all().count()
page_response = self.client.get('/tutorial-2/')
html_response = page_response.content.decode('utf8')
self.assertEqual(todo_count, 1)
self.assertIn(DUMMY_TODO_ITEM, html_response)
# 2
def test_error_create_todo_from_view(self):
response = self.client.post(
'/tutorial-2/add_todo/',
data={
'date': '',
'activity': ''
}
)
error_msg = "ERROR: Failed to add Todo List"
self.assertEqual(response.status_code, 200)
self.assertRaises(ValueError)
self.assertContains(response, error_msg)
```
**Penjelasan:** Pada method nomor 1, dilakukan `POST` request ke URL `/tutorial-2/add_todo/` dengan memasukkan data-data yang valid (memenuhi persyaratan dari properti-properti model `TodoList` ), dan dilakukan pengecekan atas 2 hal, apakah *instance* dari model `TodoList` berhasil terbentuk, dan apakah tulisan yang telah dibuat berhasil ditampilkan pada halaman website setelah di-*refresh* .
Sedangkan pada method nomor 2, dilakukan `POST` request ke URL `/tutorial-2/add_todo/` dengan memasukkan data-data yang tidak valid, dan dillakukan pengecekan atas 3 hal, apakah status code yang ditampilkan bernilai **200** (halaman input todo dengan pesan error berhasil ditampilkan), apakah *exception* `ValueError` berhasil ter-*trigger* dan apakah pesan error berhasil muncul pada halaman website setelah di-*refresh* .
Ada beberapa keuntungan dari pemisahan file-file *unit tests* dan *functional tests* atau yang disebut juga **Test Organization,** antara lain sebagai berikut.
- Pengelompokkan setiap *test case* berdasarkan fungsinya, halaman web spesifik yang akan ditest, akan membuat kode lebih tertata.
- *Maintenance* *test case* yang menjadi lebih mudah dikarenakan pengelompokkan kode *test case*
- Dapat dilakukan eksekusi *test case* tertentu saja, tidak harus mengeksekusi semua kode *test case* yang ada pada sebuah project.
\ No newline at end of file
......@@ -49,6 +49,7 @@ INSTALLED_APPS = [
'tutorial_2',
'tutorial_3',
'tutorial_4',
'tutorial_5',
]
MIDDLEWARE = [
......
/*!
* Bootstrap v3.1.1 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #2b669a;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2d6ca2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2d6ca2;
border-color: #2b669a;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #357ebd;
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
}
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #3071a9;