├── .gitignore ├── customer ├── __init__.py ├── admin.py ├── entities.py ├── forms.py ├── hydrators.py ├── mappers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_customermodel_uuid.py │ ├── 0003_auto_20150107_0230.py │ ├── 0004_auto_20150111_0232.py │ └── __init__.py ├── models.py ├── tests.py ├── unit_of_work.py ├── value_objects.py └── views.py ├── helpbase ├── __init__.py ├── exceptions.py ├── mappers.py ├── settings.py ├── templates │ └── helpbase │ │ └── home.html ├── unit_of_work.py ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── message ├── __init__.py ├── admin.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── readme.md ├── staffer ├── __init__.py ├── admin.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py └── ticket ├── __init__.py ├── admin.py ├── domain_services.py ├── entities.py ├── forms.py ├── hydrators.py ├── mappers.py ├── migrations ├── 0001_initial.py ├── 0002_ticketmodel_uuid.py ├── 0003_ticketmodel_customer.py ├── 0004_auto_20150107_0216.py ├── 0005_auto_20150107_0230.py └── __init__.py ├── models.py ├── templates └── ticket │ └── create.html ├── tests.py ├── value_objects.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | db.sqlite3 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /customer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/customer/__init__.py -------------------------------------------------------------------------------- /customer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /customer/entities.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from ticket.entities import Ticket 4 | from customer.value_objects import EmailAddress 5 | 6 | 7 | class Customer(object): 8 | __uuid = None 9 | __email_address = None 10 | __first_name = None 11 | __last_name = None 12 | __tickets = None # list of tickets 13 | 14 | def __init__( 15 | self, 16 | uuid, 17 | email_address, 18 | first_name, 19 | last_name, 20 | tickets=None 21 | ): 22 | 23 | if not isinstance(email_address, EmailAddress): 24 | raise Exception 25 | 26 | self.set_uuid(uuid) 27 | self.set_email_address(email_address) 28 | self.set_first_name(first_name) 29 | self.set_last_name(last_name) 30 | 31 | if tickets is None: 32 | tickets = [] 33 | 34 | self.__tickets = tickets 35 | 36 | def set_email_address(self, email_address): 37 | self.__email_address = email_address 38 | 39 | def get_email_address(self): 40 | return self.__email_address 41 | 42 | def set_first_name(self, first_name): 43 | self.__first_name = first_name 44 | 45 | def get_first_name(self): 46 | return self.__first_name 47 | 48 | def set_last_name(self, last_name): 49 | self.__last_name = last_name 50 | 51 | def get_last_name(self): 52 | return self.__last_name 53 | 54 | def create_ticket(self, title, body): 55 | ticket = Ticket(uuid.uuid4(), title, body, self.get_uuid()) 56 | if self.__tickets is None: 57 | self.__tickets = [] 58 | self.__tickets.append(ticket) 59 | return ticket 60 | 61 | def get_tickets(self): 62 | return self.__tickets 63 | 64 | def find_ticket_by_id(self, id): 65 | for ticket in self.get_tickets(): 66 | if ticket.get_uuid().hex == id: 67 | return ticket 68 | 69 | def close_ticket(self, ticket): 70 | ticket.close() 71 | return ticket 72 | 73 | def set_uuid(self, uuid): 74 | self.__uuid = uuid 75 | 76 | def get_uuid(self): 77 | return self.__uuid 78 | -------------------------------------------------------------------------------- /customer/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import CustomerModel 4 | 5 | 6 | class CustomerCreateForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = CustomerModel 10 | fields = ('email_address', 'first_name', 'last_name') 11 | -------------------------------------------------------------------------------- /customer/hydrators.py: -------------------------------------------------------------------------------- 1 | from ticket.hydrators import TicketHydrator 2 | 3 | 4 | class CustomerHydrator(object): 5 | 6 | def __extract(self, customer, tickets): 7 | extracted_customer = { 8 | 'uuid': customer.get_uuid(), 9 | 'email_address': customer.get_email_address(), 10 | 'first_name': customer.get_first_name(), 11 | 'last_name': customer.get_last_name(), 12 | 'tickets': tickets 13 | } 14 | return extracted_customer 15 | 16 | def hydrate(self): 17 | pass 18 | 19 | def extract(self, customer_or_customers, many=False): 20 | if many: 21 | extracted_customers = [] 22 | for customer in customer_or_customers: 23 | tickets = TicketHydrator().extract( 24 | customer.get_tickets(), True) 25 | extracted_customer = self.__extract(customer, tickets) 26 | extracted_customers.append(extracted_customer) 27 | return extracted_customers 28 | else: 29 | tickets = TicketHydrator().extract(customer.get_tickets(), True) 30 | extracted_customer = self.__extract(customer, tickets) 31 | return extracted_customer 32 | -------------------------------------------------------------------------------- /customer/mappers.py: -------------------------------------------------------------------------------- 1 | from customer.models import CustomerModel 2 | from customer.entities import Customer 3 | from ticket.mappers import TicketMapper 4 | from helpbase.mappers import AbstractDataMapper 5 | from customer.value_objects import EmailAddress 6 | 7 | 8 | class CustomerMapper(AbstractDataMapper): 9 | 10 | def find_all(self): 11 | customer_models = CustomerModel.objects.all() 12 | 13 | customer_entities = [] 14 | for customer_model in customer_models: 15 | ticket_entities = TicketMapper().get_by_customer_id( 16 | customer_model.uuid) 17 | customer_entity = self.__load_entity( 18 | customer_model, ticket_entities) 19 | customer_entities.append(customer_entity) 20 | return customer_entities 21 | 22 | def find_by_id(self, id): 23 | customer_model = CustomerModel.objects.get(uuid=id) 24 | ticket_entities = TicketMapper().get_by_customer_id(id) 25 | customer = self.__load_entity(customer_model, ticket_entities) 26 | return customer 27 | 28 | def find_by_email_address(self, email_address): 29 | customer_model = CustomerModel.objects.get(email_address=email_address) 30 | ticket_entities = TicketMapper().get_by_customer_id( 31 | customer_model.uuid) 32 | customer = self.__load_entity(customer_model, ticket_entities) 33 | return customer 34 | 35 | def create(self, customer): 36 | CustomerModel( 37 | uuid=customer.get_uuid(), 38 | email_address=customer.get_email_address(), 39 | first_name=customer.get_first_name(), 40 | last_name=customer.get_last_name() 41 | ).save() 42 | 43 | def update(self, customer): 44 | customer_model = CustomerModel.objects.get(uuid=customer.get_uuid()) 45 | customer_model.first_name = customer.get_first_name() 46 | customer_model.last_name = customer.get_last_name() 47 | customer_model.email_address = customer.get_email_address() 48 | customer_model.save() 49 | 50 | def delete(self, entity): 51 | print 'implement delete' 52 | 53 | def __load_entity(self, customer_model, ticket_entities): 54 | customer_entity = Customer( 55 | customer_model.uuid, 56 | EmailAddress(customer_model.email_address), 57 | customer_model.first_name, 58 | customer_model.last_name, 59 | ticket_entities 60 | ) 61 | return customer_entity 62 | -------------------------------------------------------------------------------- /customer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='CustomerModel', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('email_address', models.EmailField(max_length=254)), 18 | ('first_name', models.CharField(max_length=100)), 19 | ('last_name', models.CharField(max_length=100)), 20 | ], 21 | options={ 22 | }, 23 | bases=(models.Model,), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /customer/migrations/0002_customermodel_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuidfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('customer', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='customermodel', 17 | name='uuid', 18 | field=uuidfield.fields.UUIDField(default=1, max_length=32), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /customer/migrations/0003_auto_20150107_0230.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuidfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('customer', '0002_customermodel_uuid'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='customermodel', 17 | name='id', 18 | ), 19 | migrations.AlterField( 20 | model_name='customermodel', 21 | name='uuid', 22 | field=uuidfield.fields.UUIDField(max_length=32, serialize=False, primary_key=True), 23 | preserve_default=True, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /customer/migrations/0004_auto_20150111_0232.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('customer', '0003_auto_20150107_0230'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='customermodel', 16 | name='email_address', 17 | field=models.EmailField(unique=True, max_length=254), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /customer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/customer/migrations/__init__.py -------------------------------------------------------------------------------- /customer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from uuidfield import UUIDField 4 | 5 | 6 | class CustomerModel(models.Model): 7 | 8 | uuid = UUIDField(primary_key=True, auto=False) 9 | 10 | # TODO: set unique to True and just override it on the form 11 | # since we want the form to be valid even if the email address 12 | # already exists in the database. The mapper will be responsible for 13 | # making sure that there will be no duplicated email addresses in the db. 14 | # We still want to set unique=True in the model though just to make sure. 15 | email_address = models.EmailField(max_length=254, unique=False) 16 | 17 | first_name = models.CharField(max_length=100, blank=True) 18 | last_name = models.CharField(max_length=100, blank=True) 19 | -------------------------------------------------------------------------------- /customer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /customer/unit_of_work.py: -------------------------------------------------------------------------------- 1 | from ticket.mappers import TicketMapper 2 | from helpbase.unit_of_work import UnitOfWork 3 | from customer.mappers import CustomerMapper 4 | 5 | 6 | class CustomerUnitOfWork(UnitOfWork): 7 | 8 | __ticket_mapper = None 9 | __ticket_storage = None 10 | 11 | def __init__(self): 12 | super(CustomerUnitOfWork, self).__init__(CustomerMapper()) 13 | 14 | # TODO: check if we can set __ticket_mapper = TicketMapper() 15 | # directly at the top? 16 | self.__ticket_mapper = TicketMapper() 17 | 18 | if self.__ticket_storage is None: 19 | self.__ticket_storage = {} 20 | 21 | def find_all(self): 22 | customer_entities = super(CustomerUnitOfWork, self).find_all() 23 | 24 | for customer_entity in customer_entities: 25 | for ticket in customer_entity.get_tickets(): 26 | self.ticket_register_clean(ticket) 27 | 28 | return customer_entities 29 | 30 | def find_by_id(self, id): 31 | entity = super(CustomerUnitOfWork, self).find_by_id(id) 32 | 33 | for ticket in entity.get_tickets(): 34 | self.ticket_register_clean(ticket) 35 | 36 | return entity 37 | 38 | def find_by_email_address(self, email_address): 39 | entity = self._mapper.find_by_email_address(email_address) 40 | self.register_clean(entity) 41 | 42 | for ticket in entity.get_tickets(): 43 | self.ticket_register_clean(ticket) 44 | 45 | return entity 46 | 47 | def ticket_register_new(self, entity): 48 | self.__ticket_register_entity(entity, self.STATE_NEW) 49 | 50 | def ticket_register_clean(self, entity): 51 | self.__ticket_register_entity(entity, self.STATE_CLEAN) 52 | 53 | def ticket_register_dirty(self, entity): 54 | self.__ticket_register_entity(entity, self.STATE_DIRTY) 55 | 56 | def commit(self): 57 | super(CustomerUnitOfWork, self).commit() 58 | for entity in self._storage: 59 | for ticket_entity in entity.get_tickets(): 60 | if self.__ticket_storage[ticket_entity] == self.STATE_NEW: 61 | self.__ticket_mapper.create(ticket_entity) 62 | elif self.__ticket_storage[ticket_entity] == self.STATE_DIRTY: 63 | self.__ticket_mapper.update(ticket_entity) 64 | elif self.__ticket_storage[ticket_entity] == \ 65 | self.STATE_REMOVED: 66 | self.__ticket_mapper.delete(ticket_entity) 67 | 68 | def __ticket_register_entity(self, entity, state): 69 | self.__ticket_storage[entity] = state 70 | -------------------------------------------------------------------------------- /customer/value_objects.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class InvalidEmailAddressException(Exception): 5 | pass 6 | 7 | 8 | class EmailAddress(object): 9 | 10 | __email_address = None 11 | 12 | def __init__(self, email_address): 13 | if re.match(r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"'r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', email_address, re.IGNORECASE): 14 | self.__email_address = email_address 15 | else: 16 | raise InvalidEmailAddressException 17 | 18 | def __repr__(self): 19 | return self.__email_address 20 | -------------------------------------------------------------------------------- /customer/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /helpbase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/helpbase/__init__.py -------------------------------------------------------------------------------- /helpbase/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidInputsException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /helpbase/mappers.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class AbstractDataMapper(object): 5 | __metaclass__ = abc.ABCMeta 6 | 7 | @abc.abstractmethod 8 | def find_all(self): 9 | return 10 | 11 | @abc.abstractmethod 12 | def find_by_id(self, id): 13 | return 14 | 15 | @abc.abstractmethod 16 | def create(self): 17 | return 18 | 19 | @abc.abstractmethod 20 | def update(self): 21 | return 22 | 23 | @abc.abstractmethod 24 | def delete(self): 25 | return 26 | -------------------------------------------------------------------------------- /helpbase/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for helpbase project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = ')c+d)_q^@j0^xi3+gs+=#!xsapgyv8a2(axwnvo0x0x(t*&pbv' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | 'django_extensions', 41 | 42 | 'helpbase', 43 | 'customer', 44 | 'ticket', 45 | ) 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | # 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ) 56 | 57 | ROOT_URLCONF = 'helpbase.urls' 58 | 59 | WSGI_APPLICATION = 'helpbase.wsgi.application' 60 | 61 | 62 | # Database 63 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 64 | 65 | DATABASES = { 66 | 'default': { 67 | 'ENGINE': 'django.db.backends.sqlite3', 68 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 69 | } 70 | } 71 | 72 | # Internationalization 73 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 74 | 75 | LANGUAGE_CODE = 'en-us' 76 | 77 | TIME_ZONE = 'UTC' 78 | 79 | USE_I18N = True 80 | 81 | USE_L10N = True 82 | 83 | USE_TZ = True 84 | 85 | 86 | # Static files (CSS, JavaScript, Images) 87 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 88 | 89 | STATIC_URL = '/static/' 90 | -------------------------------------------------------------------------------- /helpbase/templates/helpbase/home.html: -------------------------------------------------------------------------------- 1 |

Customers

2 | 21 | -------------------------------------------------------------------------------- /helpbase/unit_of_work.py: -------------------------------------------------------------------------------- 1 | class UnitOfWork(object): 2 | 3 | STATE_NEW = 'NEW' 4 | STATE_CLEAN = 'CLEAN' 5 | STATE_DIRTY = 'DIRTY' 6 | STATE_REMOVED = 'REMOVED' 7 | 8 | _mapper = None 9 | _storage = None 10 | 11 | def __init__(self, mapper): 12 | self._mapper = mapper 13 | 14 | if self._storage is None: 15 | self._storage = {} 16 | 17 | def find_all(self): 18 | entities = self._mapper.find_all() 19 | 20 | for entity in entities: 21 | self.register_clean(entity) 22 | 23 | return entities 24 | 25 | def find_by_id(self, id): 26 | entity = self._mapper.find_by_id(id) 27 | self.register_clean(entity) 28 | return entity 29 | 30 | def register_new(self, entity): 31 | self.__register_entity(entity, self.STATE_NEW) 32 | 33 | def register_clean(self, entity): 34 | self.__register_entity(entity, self.STATE_CLEAN) 35 | 36 | def register_dirty(self, entity): 37 | self.__register_entity(entity, self.STATE_DIRTY) 38 | 39 | def register_removed(self, entity): 40 | self.__register_entity(entity, self.STATE_REMOVED) 41 | 42 | def commit(self): 43 | for entity in self._storage: 44 | if self._storage[entity] == self.STATE_NEW: 45 | self._mapper.create(entity) 46 | elif self._storage[entity] == self.STATE_DIRTY: 47 | self._mapper.update(entity) 48 | elif self._storage[entity] == self.STATE_REMOVED: 49 | self._mapper.delete(entity) 50 | 51 | # TODO: implement this 52 | def rollback(self): 53 | pass 54 | 55 | def clear(self): 56 | self._storage = {} 57 | return self 58 | 59 | def __register_entity(self, entity, state): 60 | self._storage[entity] = state 61 | -------------------------------------------------------------------------------- /helpbase/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | from .views import HomeView 5 | from ticket.views import TicketCreateView 6 | 7 | 8 | urlpatterns = patterns( 9 | '', 10 | url(r'^admin/', include(admin.site.urls)), 11 | url(r'^$', HomeView.as_view(), name='home'), 12 | url(r'^tickets/create', TicketCreateView.as_view()), 13 | ) 14 | -------------------------------------------------------------------------------- /helpbase/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | 3 | from customer.unit_of_work import CustomerUnitOfWork 4 | from customer.hydrators import CustomerHydrator 5 | 6 | 7 | class HomeView(ListView): 8 | 9 | template_name = 'helpbase/home.html' 10 | 11 | def get_queryset(self): 12 | session = CustomerUnitOfWork() 13 | return CustomerHydrator().extract(session.find_all(), True) 14 | -------------------------------------------------------------------------------- /helpbase/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for helpbase project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helpbase.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helpbase.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /message/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/message/__init__.py -------------------------------------------------------------------------------- /message/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /message/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/message/migrations/__init__.py -------------------------------------------------------------------------------- /message/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /message/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /message/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Help Base 2 | ======================== 3 | 4 | An attempt to implement DDD and hexagonal architecture in Python using Django framework w/o replacing Django's core components. 5 | 6 | Domain 7 | ======================== 8 | 9 | - A customer should be able to create a ticket. 10 | - A customer should have an email address, first name and last name. An email address should always be present. 11 | - Customer records should be unique. When a customer creates a ticket for the first time, a record of that customer will be created. This record will be used for the next tickets that he creates. 12 | - A ticket should have a title and a body. A title should always be present. 13 | - A ticket is assigned a Staffer at some point (not on creation). 14 | - A staffer can assign himself to a ticket. Only one staffer is allowed to be assigned on each ticket. 15 | - A staffer or a customer can close a ticket. 16 | - If a ticket has a status of "closed", any newly received message will re-open the ticket. 17 | -------------------------------------------------------------------------------- /staffer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/staffer/__init__.py -------------------------------------------------------------------------------- /staffer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /staffer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/staffer/migrations/__init__.py -------------------------------------------------------------------------------- /staffer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /staffer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /staffer/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /ticket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/ticket/__init__.py -------------------------------------------------------------------------------- /ticket/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /ticket/domain_services.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from customer.entities import Customer 4 | from customer.models import CustomerModel 5 | from customer.unit_of_work import CustomerUnitOfWork 6 | from helpbase.exceptions import InvalidInputsException 7 | from customer.value_objects import EmailAddress 8 | from customer.forms import CustomerCreateForm 9 | from ticket.value_objects import Title 10 | from ticket.forms import TicketCreateForm 11 | 12 | 13 | class TicketCreateService(object): 14 | 15 | __customer_create_form = None 16 | __ticket_create_form = None 17 | 18 | def run( 19 | self, 20 | email_address, 21 | first_name, 22 | last_name, 23 | title, 24 | body 25 | ): 26 | inputs = { 27 | 'email_address': email_address, 28 | 'first_name': first_name, 29 | 'last_name': last_name, 30 | 'title': title, 31 | 'body': body 32 | } 33 | 34 | # NOTE: we are depending on these 2 forms that extends 35 | # django.forms.ModelForm, but since we are only instantiating them and 36 | # only calling the is_valid methods, I guess it's fine. I think there's 37 | # no point in creating a class that will wrap them. 38 | ticket_create_form = TicketCreateForm(inputs) 39 | customer_create_form = CustomerCreateForm(inputs) 40 | 41 | self.__ticket_create_form = ticket_create_form 42 | self.__customer_create_form = customer_create_form 43 | 44 | if customer_create_form.is_valid() and ticket_create_form.is_valid(): 45 | session = CustomerUnitOfWork() 46 | 47 | try: 48 | customer = session.find_by_email_address(email_address) 49 | ticket = customer.create_ticket(Title(title), body) 50 | session.ticket_register_new(ticket) 51 | except CustomerModel.DoesNotExist: 52 | customer = Customer( 53 | uuid.uuid4(), 54 | EmailAddress(email_address), 55 | first_name, 56 | last_name 57 | ) 58 | session.register_new(customer) 59 | ticket = customer.create_ticket( 60 | Title(title), 61 | body 62 | ) 63 | session.ticket_register_new(ticket) 64 | session.commit() 65 | else: 66 | raise InvalidInputsException 67 | 68 | def get_form(self): 69 | return (self.__customer_create_form, self.__ticket_create_form) 70 | -------------------------------------------------------------------------------- /ticket/entities.py: -------------------------------------------------------------------------------- 1 | from ticket.value_objects import Title 2 | 3 | 4 | class Ticket(object): 5 | __uuid = None 6 | __title = None 7 | __body = None 8 | __customer_id = None 9 | 10 | def __init__(self, uuid, title, body, customer_id): 11 | if not isinstance(title, Title): 12 | raise Exception 13 | 14 | self.set_uuid(uuid) 15 | self.set_title(title) 16 | self.set_body(body) 17 | self.set_customer_id(customer_id) 18 | 19 | def set_title(self, title): 20 | self.__title = title 21 | 22 | def get_title(self): 23 | return self.__title 24 | 25 | def set_body(self, body): 26 | self.__body = body 27 | 28 | def get_body(self): 29 | return self.__body 30 | 31 | def set_uuid(self, uuid): 32 | self.__uuid = uuid 33 | 34 | def get_uuid(self): 35 | return self.__uuid 36 | 37 | def set_customer_id(self, customer_id): 38 | self.__customer_id = customer_id 39 | 40 | def get_customer_id(self): 41 | return self.__customer_id 42 | 43 | def close(self): 44 | pass 45 | -------------------------------------------------------------------------------- /ticket/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import TicketModel 4 | 5 | 6 | class TicketCreateForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = TicketModel 10 | fields = ('title', 'body') 11 | -------------------------------------------------------------------------------- /ticket/hydrators.py: -------------------------------------------------------------------------------- 1 | class TicketHydrator(object): 2 | 3 | def __extract(self, ticket): 4 | extracted_ticket = { 5 | 'uuid': ticket.get_uuid(), 6 | 'title': ticket.get_title(), 7 | 'body': ticket.get_body() 8 | } 9 | return extracted_ticket 10 | 11 | def hydrate(self): 12 | pass 13 | 14 | def extract(self, ticket_or_tickets, many=False): 15 | if many: 16 | extracted_tickets = [] 17 | for ticket in ticket_or_tickets: 18 | extracted_ticket = self.__extract(ticket) 19 | extracted_tickets.append(extracted_ticket) 20 | return extracted_tickets 21 | else: 22 | extracted_ticket = self.__extract(ticket) 23 | return extracted_ticket 24 | -------------------------------------------------------------------------------- /ticket/mappers.py: -------------------------------------------------------------------------------- 1 | from ticket.models import TicketModel 2 | from ticket.entities import Ticket 3 | from ticket.value_objects import Title 4 | from helpbase.mappers import AbstractDataMapper 5 | 6 | 7 | class TicketMapper(AbstractDataMapper): 8 | 9 | def find_all(self): 10 | pass 11 | 12 | def find_by_id(self, customer_id): 13 | pass 14 | 15 | def create(self, ticket): 16 | TicketModel( 17 | uuid=ticket.get_uuid(), 18 | title=ticket.get_title(), 19 | body=ticket.get_body(), 20 | customer_uuid=ticket.get_customer_id() 21 | ).save() 22 | 23 | def update(self, ticket): 24 | ticket_model = TicketModel.objects.get(uuid=ticket.get_uuid()) 25 | ticket_model.title = ticket.get_title() 26 | ticket_model.body = ticket.get_body() 27 | ticket_model.save() 28 | 29 | def delete(self): 30 | pass 31 | 32 | def get_by_customer_id(self, customer_id): 33 | ticket_models = TicketModel.objects.filter(customer_uuid=customer_id) 34 | 35 | ticket_entities = [] 36 | for ticket_model in ticket_models: 37 | ticket_entity = self.__load_entity(ticket_model) 38 | ticket_entities.append(ticket_entity) 39 | return ticket_entities 40 | 41 | def __load_entity(self, ticket_model): 42 | ticket_entity = Ticket( 43 | ticket_model.uuid, 44 | Title(ticket_model.title), 45 | ticket_model.body, 46 | ticket_model.customer_uuid 47 | ) 48 | return ticket_entity 49 | -------------------------------------------------------------------------------- /ticket/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='TicketModel', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('title', models.CharField(max_length=100)), 18 | ('body', models.TextField()), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /ticket/migrations/0002_ticketmodel_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuidfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('ticket', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='ticketmodel', 17 | name='uuid', 18 | field=uuidfield.fields.UUIDField(default=1, max_length=32), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ticket/migrations/0003_ticketmodel_customer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('customer', '0001_initial'), 11 | ('ticket', '0002_ticketmodel_uuid'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='ticketmodel', 17 | name='customer', 18 | field=models.ForeignKey(related_name='tickets', default=1, to='customer.CustomerModel'), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ticket/migrations/0004_auto_20150107_0216.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuidfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('ticket', '0003_ticketmodel_customer'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='ticketmodel', 17 | name='id', 18 | ), 19 | migrations.AlterField( 20 | model_name='ticketmodel', 21 | name='uuid', 22 | field=uuidfield.fields.UUIDField(max_length=32, serialize=False, primary_key=True), 23 | preserve_default=True, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /ticket/migrations/0005_auto_20150107_0230.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import uuidfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('ticket', '0004_auto_20150107_0216'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='ticketmodel', 17 | name='customer', 18 | ), 19 | migrations.AddField( 20 | model_name='ticketmodel', 21 | name='customer_uuid', 22 | field=uuidfield.fields.UUIDField(default=1, max_length=32), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /ticket/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnnncodes/ddd-python-django/22ea2874a0ad9c526bb69acea7cde6bb68d8be44/ticket/migrations/__init__.py -------------------------------------------------------------------------------- /ticket/models.py: -------------------------------------------------------------------------------- 1 | from uuidfield import UUIDField 2 | 3 | from django.db import models 4 | 5 | 6 | class TicketModel(models.Model): 7 | 8 | uuid = UUIDField(primary_key=True, auto=False) 9 | title = models.CharField(max_length=100) 10 | body = models.TextField(blank=True) 11 | customer_uuid = UUIDField(auto=False) 12 | -------------------------------------------------------------------------------- /ticket/templates/ticket/create.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | {% if customer_create_form.errors or ticket_create_form.errors %} 5 |
6 |

We encountered some errors:

7 | 8 | 35 |
36 | {% endif %} 37 | 38 |

Customer Info

39 | 40 |
41 | 42 | 47 |
48 |
49 | 50 | 55 |
56 |
57 | 58 | 63 |
64 | 65 |

Ticket

66 | 67 |
68 | 69 | 75 |
76 |
77 | 78 | 82 |
83 | 84 |
85 | -------------------------------------------------------------------------------- /ticket/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /ticket/value_objects.py: -------------------------------------------------------------------------------- 1 | class InvalidTitleException(Exception): 2 | pass 3 | 4 | 5 | class Title(object): 6 | 7 | __value = None 8 | 9 | def __init__(self, value): 10 | if value: 11 | self.__value = value 12 | else: 13 | raise InvalidTitleException 14 | 15 | def __repr__(self): 16 | return self.__value 17 | -------------------------------------------------------------------------------- /ticket/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.views.generic import View 3 | 4 | from customer.forms import CustomerCreateForm 5 | from ticket.forms import TicketCreateForm 6 | from ticket.domain_services import TicketCreateService 7 | from helpbase.exceptions import InvalidInputsException 8 | 9 | 10 | class TicketCreateView(View): 11 | 12 | template_name = 'ticket/create.html' 13 | 14 | def get(self, request, *args, **kwargs): 15 | ticket_create_form = TicketCreateForm() 16 | customer_create_form = CustomerCreateForm() 17 | 18 | return render( 19 | request, 20 | self.template_name, 21 | { 22 | 'ticket_create_form': ticket_create_form, 23 | 'customer_create_form': customer_create_form 24 | } 25 | ) 26 | 27 | def post(self, request, *args, **kwargs): 28 | email_address = request.POST.get('email_address') 29 | first_name = request.POST.get('first_name') 30 | last_name = request.POST.get('last_name') 31 | ticket_title = request.POST.get('title') 32 | ticket_body = request.POST.get('body') 33 | 34 | ticketCreateService = TicketCreateService() 35 | 36 | try: 37 | ticketCreateService.run( 38 | email_address, 39 | first_name, 40 | last_name, 41 | ticket_title, 42 | ticket_body 43 | ) 44 | return redirect('home') 45 | except InvalidInputsException: 46 | customer_create_form, ticket_create_form = \ 47 | ticketCreateService.get_form() 48 | return render( 49 | request, 50 | self.template_name, 51 | { 52 | 'ticket_create_form': ticket_create_form, 53 | 'customer_create_form': customer_create_form 54 | } 55 | ) 56 | --------------------------------------------------------------------------------