Add docker to project and fix tests

parent 967a4629
......@@ -14,6 +14,8 @@ tests/coverage_html/
tests/.coverage
build/
tests/report/
docker/nginx/collect/
docker/env
.vagrant/
cookbooks/
......
......@@ -14,4 +14,5 @@ test:
- apt-get install python3-pip -y -qq
- pip3 install -e .[dev]
- flake8 src/ --exclude migrations
- python3 manage.py test smi_unb --settings=smi_unb.settings_runner
- coverage run manage.py test smi_unb --settings=smi_unb.settings_runner
- coverage report
FROM python:3.5
RUN apt-get update &&\
apt-get install --no-install-recommends --no-install-suggests -y \
\
# Extra deps
python3-pip
# Base deps
RUN pip3 install \
requests[security] \
django==1.9.8 \
psycopg2==2.6.2 \
gunicorn \
redis==2.10.3 \
django-redis==4.8.0 \
numpy \
pandas \
Sphinx \
django-polymorphic \
manuel \
mpld3 \
six \
unipath \
django_cron \
matplotlib \
djangorestframework
RUN mkdir -p /app
WORKDIR /app
COPY ["setup.py", "requirements.txt", "/app/"]
COPY [ \
"boilerplate.ini", \
"manage.py", \
"tasks.py", \
"/app/" \
]
COPY src/ /app/src/
ENV PYTHONPATH=src
\ No newline at end of file
#!/bin/bash
sudo apt-get install postgresql python-psycopg2 libpq-dev -y
ADMIN="admin"
SMIDB="smiunb"
sudo su - postgres -c "createuser vagrant --no-superuser --no-createdb --no-createrole" || true
sudo su - postgres -c "createuser $ADMIN --no-superuser --createdb --no-createrole" || true
sudo su - postgres -c "createdb $SMIDB -O $ADMIN" || true
cd /vagrant/SMI_UnB
sudo pip install -r requirements.txt
python manage.py migrate
\ No newline at end of file
......@@ -2,7 +2,7 @@
project = SMI-UnB
version = 0.1.0
has_script = false
pyname = SMI_UnB
pyname = smi_unb
license = mit
author = Brenddon Gontijo
python_version = both
......
#!/bin/bash
# Script to download all necessary cookbooks for Chef and run vagrant up.
mkdir cookbooks && cd cookbooks/
git clone https://github.com/chef-cookbooks/apt.git
git clone https://github.com/chef-cookbooks/build-essential.git
git clone https://github.com/chef-cookbooks/chef_handler.git
git clone https://github.com/chef-cookbooks/compat_resource.git
git clone https://github.com/chef-cookbooks/dmg.git
git clone https://github.com/chef-cookbooks/git.git
git clone https://github.com/chef-cookbooks/mingw.git
git clone https://github.com/chef-cookbooks/openssl.git
git clone https://github.com/chef-cookbooks/vim.git
git clone https://github.com/chef-cookbooks/windows.git
git clone https://github.com/chef-cookbooks/yum-epel.git
git clone https://github.com/chef-cookbooks/yum.git
git clone https://github.com/daptiv/seven_zip.git
git clone https://github.com/poise/python.git
git clone https://github.com/sethvargo/chef-sugar.git
cd .. && vagrant up
vagrant ssh -c "/vagrant/after_installation_script.sh"
vagrant ssh
\ No newline at end of file
version: '2'
services:
nginx:
restart: always
build: ./docker/nginx/
ports:
- "80:80"
volumes:
- ./docker/nginx/collect/static/:/var/www/static/
volumes_from:
- web
links:
- web:web
web:
build: .
expose:
- "8000"
links:
- postgres:postgres
- redis:redis
env_file: ./docker/env
volumes:
- ./src/:/app/src/
command: bash -c "python3 manage.py makemigrations && python3 manage.py migrate && python3 manage.py loaddata src/smi_unb/fixtures/initial_data.json && gunicorn smi_unb.wsgi -b 0.0.0.0:8000"
postgres:
restart: always
image: postgres:latest
volumes_from:
- data
volumes:
- ./docker/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
env_file:
- ./docker/env
expose:
- "5432"
redis:
restart: always
image: redis:latest
expose:
- "6379"
data:
restart: always
image: alpine
volumes:
- /var/lib/postgresql
command: "true"
FROM tutum/nginx
ADD /collect/static/ /var/www/static/
RUN rm /etc/nginx/sites-enabled/default
ADD sites-enabled/ /etc/nginx/sites-enabled
\ No newline at end of file
server {
listen 80;
client_max_body_size 4G;
server_name localhost;
charset utf-8;
location /static/ {
alias /var/www/static/;
open_file_cache max=1000 inactive=40s;
open_file_cache_valid 60s;
open_file_cache_min_uses 5;
open_file_cache_errors off;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_pass http://web:8000;
}
}
\ No newline at end of file
#!/bin/env bash
psql -U postgres -c "CREATE USER $DB_USER PASSWORD '$DB_PASS'"
psql -U postgres -c "CREATE DATABASE $DB_NAME OWNER $DB_USER"
\ No newline at end of file
......@@ -23,7 +23,7 @@ with open(path, 'w') as F:
setup(
# Basic info
name=u'SMI-UnB',
name=u'smi-unb',
version=version,
author='Brenddon Gontijo',
author_email='brenddongontijo@msn.com',
......
......@@ -21,15 +21,19 @@ class EnergyTransductorSerializer(serializers.ModelSerializer):
'broken', 'last_measurement_sent')
class BuildingSerializer(serializers.ModelSerializer):
class BuildingSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField()
campus = serializers.SerializerMethodField()
class Meta:
model = Building
fields = (
'id', 'campus', 'name', 'acronym', 'description', 'phone',
'url', 'id', 'campus', 'name', 'acronym', 'description', 'phone',
'website_address', 'server_ip_address', 'active')
def get_campus(self, obj):
return obj.campus.name
class EnergyMeasurementsSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
......
......@@ -70,18 +70,19 @@ class InstanceSynchronizer(object):
class SyncUtils(object):
@classmethod
def generate_endpoint_url(
cls, slave_ip, new, api_view_set, instance_id='', query_params=''
cls, slave_ip, new, api_view_set,
instance_id='', query_params='', port=''
):
endpoint_url = ''
if new:
endpoint_url = reverse('api:' + api_view_set + '-list')
endpoint_url = reverse(api_view_set + '-list')
else:
endpoint_url = reverse(
'api:' + api_view_set + '-detail', args=(instance_id, ))
api_view_set + '-detail', args=(instance_id, ))
url = 'http://' + slave_ip + ':' \
+ '3000' + endpoint_url + query_params
+ port + endpoint_url + query_params
return url
......@@ -125,7 +126,7 @@ class EnergyMeasurementSynchronizer(object):
for building in self.buildings:
url = SyncUtils.generate_endpoint_url(
building.server_ip_address, True,
'energy_measurements', query_params=query_params
'energymeasurements', query_params=query_params
)
measurements = SyncUtils.get_request_data(url)
......@@ -150,7 +151,7 @@ class EnergyMeasurementSynchronizer(object):
url = SyncUtils.generate_endpoint_url(
building.server_ip_address, True,
'energy_transductors', query_params='?update_collection=true'
'energytransductors', query_params='?update_collection=true'
)
json_data = SyncUtils.create_json_data(
......
......@@ -41,6 +41,7 @@ class EnergyTransductorViewSet(viewsets.ModelViewSet):
class EnergyMeasurementsViewSet(viewsets.ModelViewSet):
queryset = EnergyMeasurements.objects.all()
serializer_class = EnergyMeasurementsSerializer
def get_queryset(self):
......
import requests
from django import forms
from django.utils.translation import ugettext as _
from .models import Building
......@@ -105,6 +109,25 @@ class BuildingForm(forms.ModelForm):
)
)
def is_valid(self):
valid = super(BuildingForm, self).is_valid()
if not valid:
return valid
url = 'http://' + self.instance.server_ip_address
try:
requests.get(url)
except requests.ConnectionError:
self.add_error(
None,
_('Não foi possível comunicar com o servidor do Edifício. ' +
'Verifique o endereço de IP informado.')
)
return False
return True
class EnableBuildingForm(forms.ModelForm):
class Meta:
......@@ -120,6 +143,17 @@ class EnableBuildingForm(forms.ModelForm):
if self.instance.active:
return False
url = 'http://' + self.instance.server_ip_address
try:
requests.get(url)
except requests.ConnectionError:
self.add_error(
None,
_('Não foi possível comunicar com o servidor do Edifício. ' +
'Verifique o endereço de IP informado.')
)
return False
return True
......
......@@ -69,4 +69,4 @@ class Building(models.Model):
return transductors_updated
def api_view_set(self):
return 'buildings'
return 'building'
import mock
import requests
from django.test import TestCase
from smi_unb.campuses.models import AdministrativeRegion, Campus
......@@ -30,7 +33,8 @@ class BuildingsFormsTest(TestCase):
return building
def test_building_form_valid(self):
@mock.patch('requests.get', return_value=True)
def test_building_form_valid(self, mock):
data = {
'campus': self.building.campus.id,
'name': 'Test Name',
......@@ -60,7 +64,24 @@ class BuildingsFormsTest(TestCase):
self.assertFalse(form.is_valid())
def test_enable_building_form_valid(self):
@mock.patch('requests.get', side_effect=requests.ConnectionError('Error'))
def test_building_form_not_connecting_to_server(self, mock):
data = {
'campus': self.building.campus.id,
'name': 'Test Name',
'acronym': 'Test',
'server_ip_address': '1.2.3.4'
}
form = BuildingForm(
data=data,
campus=self.building.campus
)
self.assertFalse(form.is_valid())
@mock.patch('requests.get', return_value=True)
def test_enable_building_form_valid(self, mock):
self.building.active = False
data = {
......@@ -74,6 +95,21 @@ class BuildingsFormsTest(TestCase):
self.assertTrue(form.is_valid())
@mock.patch('requests.get', side_effect=requests.ConnectionError('Error'))
def test_enable_building_not_connecting_to_server(self, mock):
self.building.active = False
data = {
'enable': ''
}
form = EnableBuildingForm(
data=data,
instance=self.building
)
self.assertFalse(form.is_valid())
def test_enable_building_form_invalid_fields(self):
form = EnableBuildingForm(
instance=self.building
......
import mock
import requests
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.test.client import Client
from django.test import TestCase
from smi_unb.api.synchronization import SyncManager
from smi_unb.campuses.models import AdministrativeRegion, Campus
from smi_unb.transductor.models import EnergyTransductor, TransductorModel
from smi_unb.users.models import UserPermissions
......@@ -183,7 +185,9 @@ class BuildingsViewsTests(TestCase):
'''
Tests Create Building
'''
def test_crete_new_building(self):
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch('requests.get', return_value=True)
def test_crete_new_building(self, mock_get, mock_sync):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -215,7 +219,9 @@ class BuildingsViewsTests(TestCase):
)
)
def test_not_create_new_building_invalid_params(self):
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch('requests.get', return_value=True)
def test_not_create_new_building_invalid_params(self, mock_1, mock_2):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -240,10 +246,36 @@ class BuildingsViewsTests(TestCase):
self.assertEqual(old_building_count, new_building_count)
@mock.patch('requests.get', side_effect=requests.ConnectionError('Error'))
def test_not_create_new_building_server_connection_failed(self, mock):
UserPermissions.add_user_permissions(self.user, ['manager_buildings'])
self.client.login(username=self.user.email, password='password')
url = reverse(
'buildings:new_building',
kwargs={'campus_string': self.building.campus.url_param}
)
params = {
'campus': self.building.campus.id,
'name': 'Test Building 2',
'server_ip_address': '2.2.2.2'
}
response = self.client.post(url, params)
error_msg = 'Não foi possível comunicar com o servidor do Edifício.'
error_msg += ' Verifique o endereço de IP informado.'
self.assertFormError(response, 'form', None, error_msg)
'''
Tests Edit Building
'''
def test_edit_building(self):
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch('requests.get', return_value=True)
def test_edit_building(self, mock_get, mock_sync):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -272,8 +304,11 @@ class BuildingsViewsTests(TestCase):
self.assertEqual(new_name, edited_building.name)
self.assertEqual(new_ip_address, edited_building.server_ip_address)
self.assertEqual(True, edited_building.synchronized)
def test_not_edit_building_wrong_params(self):
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch('requests.get', return_value=True)
def test_not_edit_building_wrong_params(self, mock_get, mock_sync):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -299,9 +334,39 @@ class BuildingsViewsTests(TestCase):
error_msg = 'This field is required.'
self.assertFormError(response, 'form', 'name', error_msg)
def test_not_edit_building_without_changes(self):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
@mock.patch('requests.get', side_effect=requests.ConnectionError('Error'))
def test_not_edit_building_server_connection_failed(self, mock):
UserPermissions.add_user_permissions(self.user, ['manager_buildings'])
self.client.login(username=self.user.email, password='password')
url = reverse(
'buildings:edit_building',
kwargs={
'building_id': self.building.id,
'campus_string': self.building.campus.url_param
}
)
new_name = 'Edited Name'
new_ip_address = '4.3.2.1'
params = {
'campus': self.building.campus.id,
'name': new_name,
'server_ip_address': new_ip_address
}
response = self.client.post(url, params)
error_msg = 'Não foi possível comunicar com o servidor do Edifício.'
error_msg += ' Verifique o endereço de IP informado.'
self.assertFormError(response, 'form', None, error_msg)
@mock.patch('requests.get', return_value=True)
def test_not_edit_building_without_changes(self, mock):
UserPermissions.add_user_permissions(self.user, ['manager_buildings'])
self.client.login(username=self.user.email, password='password')
......@@ -341,15 +406,18 @@ class BuildingsViewsTests(TestCase):
self.assertTemplateUsed(response, '404.html')
'''
Tests Enable Building
'''
# '''
# Tests Enable Building
# '''
@mock.patch.object(
Building, 'set_building_active', return_value=True)
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch.object(
EnableBuildingForm, 'is_valid',
autospec=True, return_value=True)
def test_enable_building(self, mock_is_valid, mock_set_building_active):
def test_enable_building(
self, mock_is_valid, mock_sync, mock_set_building_active
):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -377,11 +445,12 @@ class BuildingsViewsTests(TestCase):
@mock.patch.object(
Building, 'set_building_active', return_value=True)
@mock.patch.object(SyncManager, 'sync_building', return_value=True)
@mock.patch.object(
EnableBuildingForm, 'is_valid',
autospec=True, return_value=True)
def test_enable_building_with_next_page_param(
self, mock_is_valid, mock_set_building_active
self, mock_is_valid, mock_sync, mock_set_building_active
):
UserPermissions.add_user_permissions(
self.user, ['manager_buildings'])
......@@ -488,9 +557,9 @@ class BuildingsViewsTests(TestCase):
)
)
'''
Tests Disable Building
'''
# '''
# Tests Disable Building
# '''
@mock.patch.object(
Building, 'set_building_inactive', return_value=True)
@mock.patch.object(
......@@ -607,9 +676,9 @@ class BuildingsViewsTests(TestCase):
)
)
'''
Tests Inactive Transductors
'''
# '''
# Tests Inactive Transductors
# '''
@mock.patch.object(
Building, 'get_all_inactive_transductors',
return_value=[])
......
......@@ -4,6 +4,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.utils.translation import ugettext as _
from smi_unb.api.synchronization import SyncManager
from smi_unb.campuses.models import Campus
from .forms import BuildingForm, EnableBuildingForm, DisableBuildingForm
from .models import Building
......@@ -40,7 +41,12 @@ def new_building(request, campus_string):
form = BuildingForm(request.POST, campus=campus)
if form.is_valid():
form.save()
building = form.save()
building.synchronized = SyncManager.sync_building(
building, new=True)
building.save()
return HttpResponseRedirect(
reverse(
......@@ -79,9 +85,13 @@ def edit_building(request, building_id, campus_string):
)
if form.is_valid():
if form.has_changed():
form.save()
building = form.save(commit=False)
building.synchronized = SyncManager.sync_building(
building)
building.save()
return HttpResponseRedirect(
reverse(
......@@ -115,8 +125,11 @@ def enable_building(request, building_id):
form = EnableBuildingForm(request.POST, instance=building)
if form.is_valid():
form.instance.set_building_active()
form.save()
building = form.save(commit=False)
building.synchronized = SyncManager.sync_building(
building)
building.set_building_active()
building.save()
next_page = request.POST.get('next')
......@@ -143,8 +156,10 @@ def disable_building(request, building_id):
form = DisableBuildingForm(request.POST, instance=building)
if form.is_valid():
form.instance.set_building_inactive()
form.save()
building = form.save(commit=False)
building.synchronized = False
building.set_building_inactive()
building.save()
return HttpResponseRedirect(
reverse(
......
......@@ -90,14 +90,41 @@ WSGI_APPLICATION = 'smi_unb.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'smiunb',
'user': 'admin',
'password': 'admin',
if 'DB_NAME' in os.environ:
# Running the Docker image
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASS'],
'HOST': os.environ['DB_SERVICE'],
'PORT': os.environ['DB_PORT']
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://redis:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
else:
# Building the Docker image
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'smiunb',
'user': 'admin',
'password': 'admin',
}
}
}
# Auth
AUTHENTICATION_BACKENDS = ('smi_unb.authentication.auth.EmailBackend',)
......@@ -135,9 +162,7 @@ USE_L10N = True