├── app ├── templates │ ├── auth │ │ ├── home.html │ │ └── login.html │ ├── core │ │ ├── home.html │ │ ├── view_contact.html │ │ ├── create_organisation.html │ │ ├── create_contact.html │ │ └── view_organisation.html │ └── macros.html ├── extensions.py ├── auth │ ├── __init__.py │ ├── controller.py │ ├── forms.py │ └── models.py ├── core │ ├── __init__.py │ ├── forms.py │ ├── controller.py │ └── models.py ├── database │ └── __init__.py └── __init__.py ├── .travis.yml ├── run.py ├── README.md ├── requirements.txt ├── config.py ├── .gitignore ├── manage.py └── tests ├── test_auth.py └── test_core.py /app/templates/auth/home.html: -------------------------------------------------------------------------------- 1 |

Home Page

-------------------------------------------------------------------------------- /app/templates/core/home.html: -------------------------------------------------------------------------------- 1 |

Home Page

-------------------------------------------------------------------------------- /app/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_bcrypt import Bcrypt 2 | bcrypt = Bcrypt() 3 | 4 | from flask_login import LoginManager 5 | login_manager = LoginManager() -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to run tests 5 | install: 6 | - pip install -r requirements.txt 7 | script: nosetests -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # Launch dev server 2 | from app import create_app 3 | 4 | application = create_app() 5 | application.run(host='0.0.0.0', port=8080, debug=True) 6 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | auth = Blueprint('auth', __name__, template_folder='templates/auth') 4 | 5 | from .models import User 6 | from . import controller -------------------------------------------------------------------------------- /app/templates/core/view_contact.html: -------------------------------------------------------------------------------- 1 | {% from 'macros.html' import display_field %} 2 | {% for column in columns %} 3 | {{ display_field(column, record) }} 4 | {% endfor %} -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from models import Base, Contact, Organisation 4 | 5 | core = Blueprint('core', __name__, template_folder='templates/core') 6 | 7 | from . import controller 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/cghall/EasyCRM.svg?branch=master)](https://travis-ci.org/cghall/EasyCRM) 2 | #EasyCRM# 3 | An open source Customer Relationship Management system powered by Flask and SQLAlchemy. 4 | 5 | **Created by [Chris Hall](www.chrishall.io)** -------------------------------------------------------------------------------- /app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 | {% from 'macros.html' import render_field %} 4 |
5 |
6 | {{ render_field(form.username) }} 7 | {{ render_field(form.password) }} 8 |
9 |

10 |

-------------------------------------------------------------------------------- /app/templates/core/create_organisation.html: -------------------------------------------------------------------------------- 1 | {% from 'macros.html' import render_field %} 2 |

Create New Organisation

3 |
4 |
5 | {{ render_field(form.name) }} 6 | {{ render_field(form.type) }} 7 | {{ render_field(form.address) }} 8 |
9 |

10 |

-------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | 5 | from app.auth import User 6 | 7 | def populate_db(): 8 | """ 9 | Adds fake data to the database. 10 | """ 11 | admin = User(username='test@gmail.com', password='shh', first_name='chris', last_name='hall') 12 | db.session.add(admin) 13 | db.session.commit() -------------------------------------------------------------------------------- /app/templates/macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
{{ field.label }} 3 |
{{ field(**kwargs)|safe }} 4 | {% if field.errors %} 5 | 10 | {% endif %} 11 |
12 | {% endmacro %} 13 | 14 | {% macro display_field(field, record) %} 15 |

{{ field }} {{ record[field] }}

16 | {% endmacro %} -------------------------------------------------------------------------------- /app/templates/core/create_contact.html: -------------------------------------------------------------------------------- 1 | {% from 'macros.html' import render_field %} 2 |

Create New Contact

3 |
4 |
5 | {{ render_field(form.first_name) }} 6 | {{ render_field(form.last_name) }} 7 | {{ render_field(form.email) }} 8 | {{ render_field(form.role) }} 9 | {{ render_field(form.mobile) }} 10 | {{ render_field(form.org_id) }} 11 |
12 |

13 |

-------------------------------------------------------------------------------- /app/core/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms_alchemy import ModelForm 2 | from wtforms.ext.sqlalchemy.fields import QuerySelectField 3 | 4 | from app.core.models import Contact, Organisation 5 | 6 | 7 | def available_organisations(): 8 | return Organisation.query.all() 9 | 10 | 11 | class CreateOrganisation(ModelForm): 12 | class Meta: 13 | model = Organisation 14 | 15 | 16 | class CreateContact(ModelForm): 17 | class Meta: 18 | model = Contact 19 | 20 | org_id = QuerySelectField('Organisation', query_factory=available_organisations, get_label='name') 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==2.0.0 2 | behave==1.2.5 3 | cffi==1.5.2 4 | decorator==4.0.9 5 | enum34==1.1.2 6 | Flask==0.10.1 7 | Flask-Bcrypt==0.7.1 8 | Flask-Login==0.3.2 9 | Flask-SQLAlchemy==2.1 10 | Flask-Testing==0.4.2 11 | Flask-WTF==0.12 12 | infinity==1.3 13 | intervals==0.6.0 14 | itsdangerous==0.24 15 | Jinja2==2.8 16 | MarkupSafe==0.23 17 | nose==1.3.7 18 | parse==1.6.6 19 | parse-type==0.3.4 20 | pycparser==2.14 21 | six==1.10.0 22 | SQLAlchemy==1.0.11 23 | SQLAlchemy-Utils==0.31.6 24 | validators==0.10 25 | Werkzeug==0.11.3 26 | wheel==0.24.0 27 | WTForms==2.1 28 | WTForms-Alchemy==0.15.0 29 | WTForms-Components==0.10.0 30 | -------------------------------------------------------------------------------- /app/templates/core/view_organisation.html: -------------------------------------------------------------------------------- 1 | {% from 'macros.html' import display_field %} 2 | 3 |

Organisation: {{ organisation.name }}

4 | 5 | {{ display_field('name', organisation) }} 6 | {{ display_field('type', organisation) }} 7 | {{ display_field('address', organisation) }} 8 | 9 |

Contacts

10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for contact in organisation.contacts %} 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 |
NameEmailRole
{{ contact.first_name }} {{ contact.last_name }}{{ contact.email }}{{ contact.role }}
-------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from config import BaseConfig 4 | from app.extensions import bcrypt, login_manager 5 | from app.database import db 6 | from app.core import core as core_blueprint 7 | from app.auth import auth as auth_blueprint 8 | 9 | 10 | def create_app(config=BaseConfig): 11 | app = Flask(__name__) 12 | app.config.from_object(config) 13 | 14 | register_extensions(app) 15 | register_blueprints(app) 16 | 17 | return app 18 | 19 | 20 | def register_extensions(app): 21 | bcrypt.init_app(app) 22 | login_manager.init_app(app) 23 | db.init_app(app) 24 | 25 | 26 | def register_blueprints(app): 27 | app.register_blueprint(auth_blueprint) 28 | app.register_blueprint(core_blueprint) 29 | -------------------------------------------------------------------------------- /app/auth/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template, redirect, url_for 2 | from flask_login import login_user 3 | 4 | from app.auth.forms import LoginForm 5 | from app.auth.models import User 6 | from app.extensions import login_manager 7 | from . import auth 8 | from app.database import db 9 | 10 | 11 | @login_manager.user_loader 12 | def load_user(user_id): 13 | return User.query.filter_by(username=user_id).first() 14 | 15 | 16 | @auth.route('/login/', methods=['GET', 'POST']) 17 | def login(): 18 | form = LoginForm(request.form) 19 | if form.validate_on_submit(): 20 | form.user.authenticated = True 21 | db.session.add(form.user) 22 | db.session.commit() 23 | login_user(form.user) 24 | return redirect(url_for('core.home')) 25 | return render_template('auth/login.html', form=form) 26 | 27 | -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import Form 2 | from wtforms import StringField, PasswordField 3 | from wtforms.validators import DataRequired, ValidationError 4 | from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound 5 | 6 | from .models import User 7 | 8 | 9 | class LoginForm(Form): 10 | username = StringField('Username', [DataRequired(message='Enter your Username')]) 11 | password = PasswordField('Password', [DataRequired(message='Enter your Password')]) 12 | 13 | def validate_password(form, field): 14 | try: 15 | user = User.query.filter(User.username == form.username.data).one() 16 | except (MultipleResultsFound, NoResultFound): 17 | raise ValidationError("Invalid user") 18 | if user is None: 19 | raise ValidationError("Invalid user") 20 | if not user.is_correct_password(form.password.data): 21 | raise ValidationError("Invalid password") 22 | form.user = user 23 | return True -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class BaseConfig(object): 5 | """Standard configuration options""" 6 | DEBUG = True 7 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app.db') 9 | SQLALCHEMY_MIGRATE_REPO = os.path.join(BASE_DIR, 'db_repository') 10 | WTF_CSRF_ENABLED = False 11 | DATABASE_CONNECT_OPTIONS = {} 12 | THREADS_PER_PAGE = 2 13 | SECRET_KEY = "secret" 14 | BCRYPT_LOG_ROUNDS = 12 15 | 16 | 17 | class TestConfig(BaseConfig): 18 | """Configuration for general testing""" 19 | TESTING = True 20 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 21 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'test.db') 22 | SQLALCHEMY_TRACK_MODIFICATIONS = False 23 | WTF_CSRF_ENABLED = False 24 | LOGIN_DISABLED = True 25 | BCRYPT_LOG_ROUNDS = 4 26 | 27 | 28 | class AuthTestConfig(TestConfig): 29 | """For testing authentication we want to require login to check validation works""" 30 | LOGIN_DISABLED = False 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Database files 2 | *.db 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.pyc 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | #Ipython Notebook 66 | .ipynb_checkpoints 67 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | from app.database import db, populate_db 3 | from flask_migrate import Migrate, MigrateCommand 4 | from flask_script import ( 5 | Server, 6 | Shell, 7 | Manager, 8 | prompt_bool, 9 | ) 10 | 11 | 12 | def _make_context(): 13 | return dict( 14 | app=create_app(), 15 | db=db, 16 | populate_db=populate_db 17 | ) 18 | 19 | app = create_app() 20 | 21 | migrate = Migrate(app, db) 22 | 23 | manager = Manager(app) 24 | manager.add_command('runserver', Server()) 25 | manager.add_command('shell', Shell(make_context=_make_context)) 26 | manager.add_command('db', MigrateCommand) 27 | 28 | 29 | @manager.command 30 | def create_db(num_users=5): 31 | """Creates database tables and populates them.""" 32 | db.create_all() 33 | populate_db() 34 | 35 | 36 | @manager.command 37 | def drop_db(): 38 | """Drops database tables.""" 39 | if prompt_bool('Are you sure?'): 40 | db.drop_all() 41 | 42 | 43 | @manager.command 44 | def recreate_db(): 45 | """Same as running drop_db() and create_db().""" 46 | drop_db() 47 | create_db() 48 | 49 | 50 | if __name__ == '__main__': 51 | manager.run() 52 | -------------------------------------------------------------------------------- /app/auth/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.hybrid import hybrid_property 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from app.database import db 5 | from app.core import Base 6 | from app.extensions import bcrypt 7 | 8 | 9 | class User(Base): 10 | 11 | __tablename__ = 'user' 12 | 13 | username = db.Column(db.String(128), nullable=False) 14 | _password = db.Column(db.String(192), nullable=False) 15 | first_name = db.Column(db.String(128), nullable=False) 16 | last_name = db.Column(db.String(128), nullable=False) 17 | authenticated = db.Column(db.Boolean, default=False) 18 | active = db.Column(db.Boolean, default=True) 19 | 20 | @staticmethod 21 | def create(**kwargs): 22 | u = User(**kwargs) 23 | db.session.add(u) 24 | try: 25 | db.session.commit() 26 | except IntegrityError: 27 | db.session.rollback() 28 | return u 29 | 30 | def is_active(self): 31 | return self.active 32 | 33 | def get_id(self): 34 | return self.username 35 | 36 | def is_authenticated(self): 37 | return self.authenticated 38 | 39 | def is_anonymous(self): 40 | return False 41 | 42 | @hybrid_property 43 | def password(self): 44 | return self._password 45 | 46 | @password.setter 47 | def _set_password(self, plaintext): 48 | self._password = bcrypt.generate_password_hash(plaintext) 49 | 50 | def is_correct_password(self, plaintext): 51 | return bcrypt.check_password_hash(self._password, plaintext) 52 | -------------------------------------------------------------------------------- /app/core/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template, url_for, redirect 2 | from flask_login import login_required 3 | 4 | from app.core.forms import CreateContact, CreateOrganisation 5 | from app.core.models import Contact, Organisation 6 | from . import core 7 | 8 | 9 | @core.route('/') 10 | @login_required 11 | def home(): 12 | return render_template('core/home.html') 13 | 14 | 15 | @core.route('/contact/create', methods=['GET', 'POST']) 16 | @login_required 17 | def create_contact(): 18 | form = CreateContact(request.form) 19 | if request.method == 'POST': 20 | if form.validate(): 21 | form.org_id.data = form.org_id.data.id 22 | contact = Contact.create(**form.data) 23 | return redirect(url_for('core.view_contact', con_id=contact.id)) 24 | return render_template('core/create_contact.html', form=form) 25 | 26 | 27 | @core.route('/contact/') 28 | @login_required 29 | def view_contact(con_id): 30 | contact = Contact.query.filter_by(id=con_id).first() 31 | columns = [el.name for el in Contact.__table__.columns] 32 | return render_template('core/view_contact.html', columns=columns, record=contact) 33 | 34 | 35 | @core.route('/organisation/create', methods=['GET', 'POST']) 36 | @login_required 37 | def create_organisation(): 38 | form = CreateOrganisation(request.form) 39 | if request.method == 'POST': 40 | if form.validate(): 41 | org = Organisation.create(name=form.name.data, type=form.type.data, address=form.address.data) 42 | return redirect(url_for('core.view_organisation', org_id=org.id)) 43 | return render_template('core/create_organisation.html', form=form) 44 | 45 | 46 | @core.route('/organisation/') 47 | @login_required 48 | def view_organisation(org_id): 49 | org = Organisation.query.filter_by(id=org_id).first() 50 | return render_template('core/view_organisation.html', organisation=org) 51 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask import url_for 4 | from flask_login import current_user 5 | 6 | from config import AuthTestConfig 7 | from app.auth import User 8 | from app import create_app 9 | from app.database import db 10 | 11 | 12 | class AuthTestCase(unittest.TestCase): 13 | def setUp(self): 14 | self.app = create_app(AuthTestConfig) 15 | self.app_context = self.app.test_request_context() 16 | self.app_context.push() 17 | db.app = self.app 18 | db.create_all() 19 | self.client = self.app.test_client(use_cookies=True) 20 | 21 | def tearDown(self): 22 | db.session.remove() 23 | db.drop_all() 24 | self.app_context.pop() 25 | 26 | def test_login_route(self): 27 | rv = self.client.get(url_for('auth.login')) 28 | self.assertEquals(rv.status_code, 200) 29 | 30 | def test_add_user_with_password_hashing(self): 31 | user = User.create(username='test@gmail.com', password='mysecret', first_name='chris', last_name='hall') 32 | self.assertEqual(user.username, 'test@gmail.com') 33 | self.assertNotEqual(user.password, 'mysecret', 'Password not hashed') 34 | self.assertTrue(user.is_correct_password('mysecret')) 35 | self.assertEqual(user.first_name, 'chris') 36 | self.assertEqual(user.last_name, 'hall') 37 | 38 | def test_valid_login_submit(self): 39 | with self.client: 40 | user = User.create(username='right@gmail.com', password='mysecret', first_name='chris', last_name='hall') 41 | form_data = { 42 | 'username': 'right@gmail.com', 43 | 'password': 'mysecret' 44 | } 45 | rv = self.client.post(url_for('auth.login'), data=form_data, follow_redirects=True) 46 | self.assertEquals(rv.status_code, 200) 47 | self.assertTrue(user.is_authenticated()) 48 | self.assertEquals(current_user.id, user.id) 49 | rv = self.client.get('contact/create') 50 | self.assertEquals(rv.status_code, 200) 51 | 52 | def test_incorrect_password_display_message(self): 53 | User.create(username='wrong@gmail.com', password='mysecret', first_name='chris', last_name='hall') 54 | form_data = { 55 | 'username': 'wrong@gmail.com', 56 | 'password': 'wrongpassword' 57 | } 58 | rv = self.client.post(url_for('auth.login'), data=form_data, follow_redirects=True) 59 | self.assertEquals(rv.status_code, 200) 60 | self.assertTrue('Invalid password' in rv.data) 61 | 62 | # When we are not logged in trying access login_required pages should yield 401 63 | def test_login_required(self): 64 | rv = self.client.get('/') 65 | self.assertEquals(rv.status_code, 401) 66 | rv = self.client.get('/contact/create') 67 | self.assertEquals(rv.status_code, 401) 68 | -------------------------------------------------------------------------------- /app/core/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy_utils import EmailType, ChoiceType 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from app.database import db 5 | 6 | 7 | class Base(db.Model): 8 | 9 | __abstract__ = True 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) 13 | date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) 14 | 15 | 16 | class Contact(Base): 17 | 18 | """User input fields - these fields can be set by user and are included in forms""" 19 | first_name = db.Column(db.String(60), nullable=False, info={"label": "First Name"}) 20 | last_name = db.Column(db.String(60), nullable=False, info={"label": "Last Name"}) 21 | email = db.Column(EmailType, nullable=False, info={"label": "Email"}) 22 | mobile = db.Column(db.Integer, info={"label": "Mobile"}) 23 | role = db.Column(db.String(60), info={"label": "Role"}) 24 | org_id = db.Column(db.Integer, db.ForeignKey('organisation.id'), info={"label": "Organisation"}) 25 | created_by = db.Column(db.Integer, db.ForeignKey('user.id')) 26 | 27 | activities = db.relationship('Activity', backref='contact') 28 | 29 | @staticmethod 30 | def create(**kwargs): 31 | c = Contact(**kwargs) 32 | db.session.add(c) 33 | try: 34 | db.session.commit() 35 | except IntegrityError: 36 | db.session.rollback() 37 | return c 38 | 39 | 40 | class Organisation(Base): 41 | TYPE_CHOICE = [ 42 | ('charity', 'Charity'), 43 | ('funder', 'Funder'), 44 | ('other', 'Other') 45 | ] 46 | 47 | name = db.Column(db.String(100), nullable=False) 48 | type = db.Column(ChoiceType(TYPE_CHOICE), nullable=False) 49 | address = db.Column(db.Text(180)) 50 | 51 | created_by = db.Column(db.Integer, db.ForeignKey('user.id')) 52 | 53 | contacts = db.relationship('Contact', backref='organisation') 54 | activities = db.relationship('Activity', backref='contact_lookup') 55 | 56 | @staticmethod 57 | def create(**kwargs): 58 | o = Organisation(**kwargs) 59 | db.session.add(o) 60 | try: 61 | db.session.commit() 62 | except IntegrityError: 63 | db.session.rollback() 64 | return o 65 | 66 | 67 | class Project(Base): 68 | STATUS_CHOICE = [ 69 | ('in_progress', 'In Progress'), 70 | ('completed', 'Completed') 71 | ] 72 | 73 | start_date = db.Column(db.Date) 74 | end_date = db.Column(db.Date) 75 | status = db.Column(ChoiceType(STATUS_CHOICE), nullable=False) 76 | 77 | created_by = db.Column(db.Integer, db.ForeignKey('user.id')) 78 | org_id = db.Column(db.Integer, db.ForeignKey('organisation.id')) 79 | contact_id = db.Column(db.Integer, db.ForeignKey('contact.id')) 80 | 81 | activities = db.relationship('Activity', backref='project') 82 | invoices = db.relationship('Invoice', backref='project') 83 | 84 | 85 | class Invoice(Base): 86 | 87 | issue_date = db.Column(db.Date) 88 | amount = db.Column(db.Integer, nullable=False) 89 | paid = db.Column(db.Boolean, default=False) 90 | 91 | created_by = db.Column(db.Integer, db.ForeignKey('user.id')) 92 | project_id = db.Column(db.Integer, db.ForeignKey('project.id')) 93 | 94 | 95 | class Activity(Base): 96 | 97 | subject = db.Column(db.String(100), nullable=False) 98 | detail = db.Column(db.Text) 99 | 100 | contact_id = db.Column(db.Integer, db.ForeignKey('contact.id')) 101 | org_id = db.Column(db.Integer, db.ForeignKey('organisation.id')) 102 | project_id = db.Column(db.Integer, db.ForeignKey('project.id')) 103 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from config import TestConfig 4 | from app import create_app 5 | from app.database import db 6 | from app.core import Contact, Organisation 7 | 8 | 9 | class CoreTestCase(unittest.TestCase): 10 | def setUp(self): 11 | self.app = create_app(TestConfig) 12 | self.app_context = self.app.test_request_context() 13 | self.app_context.push() 14 | db.app = self.app 15 | db.create_all() 16 | self.client = self.app.test_client(use_cookies=True) 17 | 18 | def tearDown(self): 19 | db.session.remove() 20 | db.drop_all() 21 | self.app_context.pop() 22 | 23 | def test_home_page_route(self): 24 | rv = self.client.get('/') 25 | self.assertEquals(rv.status_code, 200) 26 | 27 | def test_create_contact_route(self): 28 | rv = self.client.get('/contact/create') 29 | self.assertEquals(rv.status_code, 200) 30 | 31 | def test_create_contact_empty_data(self): 32 | data = {} 33 | rv = self.client.post('/contact/create', data=data) 34 | self.assertEquals(rv.status_code, 200) 35 | 36 | def test_create_contact_valid_form(self): 37 | org_data = { 38 | 'name': 'test charity', 39 | 'type': 'charity', 40 | 'address': '1 My Road, London' 41 | } 42 | o = Organisation.create(**org_data) 43 | data = { 44 | 'first_name': 'test', 45 | 'last_name': 'contact', 46 | 'email': 'example@test.co.uk', 47 | 'org_id': o.id 48 | } 49 | rv = self.client.post('/contact/create', data=data) 50 | self.assertEquals(rv.status_code, 302) 51 | c = Contact.query.filter_by(email=data['email']).all() 52 | self.assertEqual(len(c), 1) 53 | self.assertEqual(c[0].first_name, data['first_name']) 54 | self.assertEqual(c[0].last_name, data['last_name']) 55 | self.assertEqual(c[0].email, data['email']) 56 | 57 | def test_create_contact_email_validation(self): 58 | data = { 59 | 'first_name': 'test', 60 | 'last_name': 'contact', 61 | 'email': 'notanemailaddress' 62 | } 63 | rv = self.client.post('/contact/create', data=data) 64 | self.assertEqual(rv.status_code, 200) 65 | c = Contact.query.filter_by(email=data['email']).all() 66 | self.assertEqual(len(c), 0) 67 | 68 | def test_view_contact_route(self): 69 | org_data = { 70 | 'name': 'test charity', 71 | 'type': 'charity', 72 | 'address': '1 My Road, London' 73 | } 74 | o = Organisation.create(**org_data) 75 | con_data = { 76 | 'first_name': 'test', 77 | 'last_name': 'contact', 78 | 'email': 'example@test.co.uk', 79 | 'org_id': o.id 80 | } 81 | self.client.post('/contact/create', data=con_data) 82 | c = Contact.query.filter_by(email=con_data['email']).first() 83 | rv = self.client.get('/contact/{}'.format(c.id)) 84 | self.assertEquals(rv.status_code, 200) 85 | 86 | def test_create_organisation_route(self): 87 | rv = self.client.get('/organisation/create') 88 | self.assertEquals(rv.status_code, 200) 89 | 90 | def test_create_organisation_valid_form(self): 91 | data = { 92 | 'name': 'test charity', 93 | 'type': 'charity', 94 | 'address': '1 My Road, London' 95 | } 96 | rv = self.client.post('/organisation/create', data=data) 97 | self.assertEquals(rv.status_code, 302) 98 | o = Organisation.query.filter_by(name=data['name']).all() 99 | self.assertEqual(len(o), 1) 100 | self.assertEqual(o[0].name, data['name']) 101 | self.assertEqual(o[0].type, data['type']) 102 | self.assertEqual(o[0].address, data['address']) 103 | 104 | def test_contact_organisation_relationship(self): 105 | test_org_name = 'Test Organisation' 106 | test_org = Organisation.create(name=test_org_name, type='charity') 107 | test_contact = Contact.create(first_name='test', last_name='contact', email='example@test.co.uk', 108 | org_id=test_org.id) 109 | self.assertEquals(test_org.id, test_contact.org_id) 110 | self.assertEquals(len(test_org.contacts), 1) 111 | self.assertEquals(test_contact.organisation.name, test_org_name) 112 | 113 | 114 | if __name__ == '__main__': 115 | unittest.main() --------------------------------------------------------------------------------