Skip to content

Commit 0fa0d2f

Browse files
committed
Implement first integration with PostgreSQL DB
Adjust tests to import data from fixture file `./fixtures/articles_and_authors.json` instead of using mock values defined at `models.authors` and `models.articles`. Adjust `.gitlab-ci.yml` file in order to require postgres service, configure its variables, and install `libpq-dev` system package which is required by `libpq-dev` python package. Note, at first I thought Django's `migration` were not meant to be added to VCS. However, according to [Django's documentation](https://docs.djangoproject.com/en/1.7/topics/migrations/) it seems it is: > The migration files for each app live in a “migrations” directory inside of that app, and are designed to be committed to, and distributed as part of, its codebase. You should be making them once on your development machine and then running the same migrations on your colleagues’ machines, your staging machines, and eventually your production machines.
1 parent 12358f5 commit 0fa0d2f

File tree

10 files changed

+99
-25
lines changed

10 files changed

+99
-25
lines changed

.gitlab-ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,26 @@
33
# - https://docs.gitlab.com/ce/ci/examples/test-and-deploy-python-application-to-heroku.html
44
image: python:3.5
55

6+
services:
7+
- postgres:9.3
8+
9+
variables:
10+
# Configure postgres service (https://hub.docker.com/_/postgres/)
11+
# based on https://gitlab.com/gitlab-examples/postgres/blob/master/.gitlab-ci.yml
12+
# and http://docs.gitlab.com/ce/ci/services/postgres.html
13+
POSTGRES_DB: web_scraper_test
14+
POSTGRES_USER: web_scraper_user
15+
POSTGRES_PASSWORD: web_scraper_pwd
16+
617
test:
718
script:
19+
# installs libpq-dev which is required by psycopg2
20+
# avoiding error message like:
21+
# Error: b'You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.\n'
22+
# see more at https://pypi.python.org/pypi/psycopg2
23+
- apt-get -qq update
24+
- apt-get -y -qq install libpq-dev
25+
# install project's dependencies
826
- pip install -r requirements.txt
27+
# actually execute tests
928
- python3 manage.py test

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@
33
## About
44

55
Simple Web Scraper POC project. Aims to retrieve data from a Web Page and expose extracted data through a REST API.
6+
7+
## Note about PostgreSQL usage
8+
9+
In ubuntu environment, in order to install PostgreSQL database adapter [psycopg2](https://pypi.python.org/pypi/psycopg2) it is necessary first to install `libpq-dev` package in order to avoid error message like one below:
10+
11+
Error: b'You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.\n'

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
django==1.8.13
22
jsonpath-rw==1.4.0
33
djangorestframework==3.4.7
4+
psycopg2==2.6.2
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"model": "simple_web_scraper.author", "fields": {"profile_url": "https://techcrunch.com/author/anthony-ha/", "name": "Anthony Ha"}, "pk": 1}, {"model": "simple_web_scraper.author", "fields": {"profile_url": "https://techcrunch.com/author/sarah-perez/", "name": "Sarah Perez"}, "pk": 2}, {"model": "simple_web_scraper.article", "fields": {"url": "https://techcrunch.com/2016/10/06/amazonfresh-drops-to-14-99-per-month-for-prime-members/", "title": "AmazonFresh drops to $14.99 per month for Prime members", "authors": [1, 2], "publish_date": null, "content": "a nice content"}, "pk": 1}]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Article',
15+
fields=[
16+
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
17+
('title', models.CharField(max_length=100)),
18+
('url', models.URLField()),
19+
('publish_date', models.DateTimeField(null=True, blank=True)),
20+
('content', models.CharField(max_length=500)),
21+
],
22+
),
23+
migrations.CreateModel(
24+
name='Author',
25+
fields=[
26+
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
27+
('name', models.CharField(max_length=100)),
28+
('profile_url', models.URLField()),
29+
],
30+
),
31+
migrations.AddField(
32+
model_name='article',
33+
name='authors',
34+
field=models.ManyToManyField(to='simple_web_scraper.Author'),
35+
),
36+
]

simple_web_scraper/migrations/__init__.py

Whitespace-only changes.

simple_web_scraper/settings.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,20 @@
7575

7676
# Database
7777
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
78-
78+
# https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-django-application-on-ubuntu-14-04
79+
# https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-django-application-on-ubuntu-14-04
80+
# http://stackoverflow.com/questions/5394331/how-to-setup-postgresql-database-in-django/5421511#5421511
7981
DATABASES = {
8082
'default': {
81-
'ENGINE': 'django.db.backends.sqlite3',
82-
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
83+
'ENGINE': 'django.db.backends.postgresql_psycopg2',
84+
'NAME': 'web_scraper',
85+
'USER': 'web_scraper_user',
86+
'PASSWORD': 'web_scraper_pwd',
87+
'HOST': 'localhost',
88+
'PORT': '5432',
89+
'TEST': {
90+
'NAME': 'web_scraper_test'
91+
}
8392
}
8493
}
8594

simple_web_scraper/views.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ class ArticleList(APIView):
1717
"""
1818

1919
def get(self, request, format = None):
20-
articles = models.articles
20+
articles = Article.objects.all()
2121
serializer = ArticleSerializer(articles, many=True)
2222
return Response(serializer.data)
2323

2424
def post(self, request, format = None):
2525
serializer = ArticleSerializer(data=request.data)
2626
if serializer.is_valid():
27-
# print(serializer.data)
27+
serializer.save()
2828
return Response(serializer.data, status=status.HTTP_201_CREATED)
2929
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
3030

@@ -35,27 +35,28 @@ def get_object(self, id):
3535
id = int(id)
3636
except ValueError:
3737
raise HttpResponseBadRequest()
38-
article = next(filter(lambda a: a.id == id, models.articles), None)
39-
if article is None:
38+
try:
39+
article = Article.objects.get(pk = id)
40+
except Article.DoesNotExist:
4041
raise Http404
4142
return article
4243

4344
def get(self, request, id, format=None):
4445
article = self.get_object(id)
45-
serializer = AuthorSerializer(article)
46+
serializer = ArticleSerializer(article)
4647
return Response(serializer.data)
4748

4849
def put(self, request, id, format=None):
4950
article = self.get_object(id)
5051
serializer = ArticleSerializer(article, data=request.data)
5152
if serializer.is_valid():
52-
# print(serializer.data)
53+
serializer.save()
5354
return Response(serializer.data)
5455
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
5556

5657
def delete(self, request, id, format=None):
5758
article = self.get_object(id)
58-
# article.delete()
59+
article.delete()
5960
return Response(status=status.HTTP_204_NO_CONTENT)
6061

6162
class AuthorList(APIView):
@@ -69,14 +70,14 @@ class AuthorList(APIView):
6970
"""
7071

7172
def get(self, request, format = None):
72-
authors = models.authors
73+
authors = Author.objects.all()
7374
serializer = AuthorSerializer(authors, many=True)
7475
return Response(serializer.data)
7576

7677
def post(self, request, format = None):
7778
serializer = AuthorSerializer(data=request.data)
7879
if serializer.is_valid():
79-
# print(serializer.data)
80+
serializer.save()
8081
return Response(serializer.data, status=status.HTTP_201_CREATED)
8182
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
8283

@@ -88,8 +89,9 @@ def get_object(self, id):
8889
id = int(id)
8990
except ValueError:
9091
raise HttpResponseBadRequest()
91-
author = next(filter(lambda a: a.id == id, models.authors), None)
92-
if author is None:
92+
try:
93+
author = Author.objects.get(pk = id)
94+
except Author.DoesNotExist:
9395
raise Http404
9496
return author
9597

@@ -102,11 +104,11 @@ def put(self, request, id, format=None):
102104
author = self.get_object(id)
103105
serializer = AuthorSerializer(author, data=request.data)
104106
if serializer.is_valid():
105-
# print(serializer.data)
107+
serializer.save()
106108
return Response(serializer.data)
107109
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
108110

109111
def delete(self, request, id, format=None):
110112
author = self.get_object(id)
111-
# author.delete()
113+
author.delete()
112114
return Response(status=status.HTTP_204_NO_CONTENT)

test/test_article_endpoint.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import unittest
2-
from django.test import Client
1+
from django.test import Client, TransactionTestCase
32
import json
43
from jsonpath_rw import jsonpath, parse
54

6-
class ArticleEndpointTest(unittest.TestCase):
75

6+
class ArticleEndpointTest(TransactionTestCase):
7+
8+
fixtures = ['articles_and_authors.json']
89
endpoint = '/api/rest/v1/articles'
910

1011
def test_list(self):
@@ -17,7 +18,7 @@ def test_list(self):
1718
# see more about jsonpath at http://goessner.net/articles/JsonPath/
1819
# and its python port at https://github.com/kennknowles/python-jsonpath-rw
1920
actual_ids = [match.value for match in parse('$.[*].id').find(content_json)]
20-
self.assertEquals([3], actual_ids)
21+
self.assertEquals([1], actual_ids)
2122

2223
def test_404_when_id_doesnt_exist(self):
2324
client = Client()
@@ -27,7 +28,6 @@ def test_404_when_id_doesnt_exist(self):
2728
def test_post(self):
2829
client = Client()
2930
json_str = json.dumps({
30-
"id": 1,
3131
"title": "a nice title",
3232
"url": "http://johndoo.org",
3333
"content": "dummy content",
@@ -37,7 +37,7 @@ def test_post(self):
3737

3838
def test_delete(self):
3939
client = Client()
40-
response = client.delete(ArticleEndpointTest.endpoint + '/3')
40+
response = client.delete(ArticleEndpointTest.endpoint + '/1')
4141
self.assertEqual(response.status_code, 204)
4242

4343
def test_delete_not_found(self):

test/test_author_endpoint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import unittest
2-
from django.test import Client
1+
from django.test import Client, TransactionTestCase
32
import json
43
from jsonpath_rw import jsonpath, parse
54

65

7-
class AuthorEndpointTest(unittest.TestCase):
6+
class AuthorEndpointTest(TransactionTestCase):
87

8+
fixtures = ['articles_and_authors.json']
99
endpoint = '/api/rest/v1/authors'
1010

1111
def test_list(self):

0 commit comments

Comments
 (0)