├── .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 |
3 | {% for customer in object_list %}
4 | -
5 |
6 |
{{ customer.email_address }}
7 |
Tickets
8 |
9 | {% for ticket in customer.tickets %}
10 | - {{ ticket.title }}
11 | {% empty %}
12 | - No tickets yet.
13 | {% endfor %}
14 |
15 |
16 |
17 | {% empty %}
18 | - No customers yet.
19 | {% endfor %}
20 |
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 |
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 |
--------------------------------------------------------------------------------