├── .gitignore ├── Procfile ├── README.md ├── manage.py ├── movies ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ └── index.html ├── tests.py └── views.py ├── neomovies ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | movies.db 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn neomovies.wsgi:application 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neo4j-movies-python-neomodel 2 | 3 | ## How to setup locally 4 | 5 | ### Install dependencies 6 | 7 | ```shell 8 | python3 -m venv venv 9 | source venv/bin/activate 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ### Create the Neo4j database with correct data 14 | 15 | Go to [Neo4j's Sandbox](https://sandbox.neo4j.com/) and create a new project, select Movies under Pre Built Data. Go to `Connection details` and grab your credentials to add it to the following environment variable: 16 | 17 | ```shell 18 | export NEO4J_BOLT_URL=bolt://neo4j:password@host-or-ip:port 19 | ``` 20 | 21 | Run migrations and create your superuser (for the admin, this is using an SQLite database) 22 | 23 | ``` 24 | ./manage.py migrate 25 | ./manage.py createsuperuser 26 | ``` 27 | 28 | ### Run the server 29 | 30 | ```shell 31 | python manage.py runserver 32 | ``` 33 | 34 | Now you should be able to access http://localhost:8000 and play with the app. 35 | 36 | ## Using the Admin with the Sandbox Dataset 37 | 38 | Since the Movies dataset in the sandbox is a toy dataset, we need to make some minor changes to get it to work with our Django Admin. 39 | Primarily, we need to add unique ids to our nodes so we can open their detail view: 40 | 41 | ``` Example Movie Node 42 | { 43 | "identity": 0, 44 | "labels": [ 45 | "Movie" 46 | ], 47 | "properties": { 48 | "tagline": "Welcome to the Real World", 49 | "title": "The Matrix", 50 | "released": 1999 51 | } 52 | } 53 | ``` 54 | 55 | For simplicity, do: 56 | 57 | ``` 58 | MATCH (n) 59 | SET n.uuid = toString(id(n)) 60 | ``` 61 | 62 | Now you can log into the admin with your superuser and take a look at your Move and Person nodes. 63 | 64 | ## How to deploy to Heroku 65 | 66 | Go to your Heroku dashboard and create a new app and add its git remote to your local clone of this app. 67 | 68 | Go your Heroku's app's settings and add the `NEO4J_BOLT_URL` environment variable with the correct credentials: 69 | 70 | ```NEO4J_BOLT_URL="bolt://neo4j:password@host-or-ip:port"``` 71 | 72 | Now you can push to Heroku: 73 | 74 | ```shell 75 | git push heroku master 76 | ``` 77 | 78 | And thats all you need :) 79 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'neomovies.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /movies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/neo4j-movies-python-neomodel/d98ddb9f5c7d95de85565ee13b9e963984897ac7/movies/__init__.py -------------------------------------------------------------------------------- /movies/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin as dj_admin 2 | from django_neomodel import admin as neo_admin 3 | 4 | from .models import Movie, Person 5 | 6 | # warning! If you're using Sandbox you'll have to add uuids 7 | 8 | class MovieAdmin(dj_admin.ModelAdmin): 9 | list_display = ("title","uuid") 10 | neo_admin.register(Movie, MovieAdmin) 11 | 12 | 13 | class PersonAdmin(dj_admin.ModelAdmin): 14 | list_display = ("name","uuid") 15 | neo_admin.register(Person, PersonAdmin) -------------------------------------------------------------------------------- /movies/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MoviesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'movies' 7 | -------------------------------------------------------------------------------- /movies/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/neo4j-movies-python-neomodel/d98ddb9f5c7d95de85565ee13b9e963984897ac7/movies/migrations/__init__.py -------------------------------------------------------------------------------- /movies/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_neomodel import DjangoNode 3 | from neomodel import ArrayProperty, StringProperty, IntegerProperty, RelationshipFrom, RelationshipTo, StructuredRel, UniqueIdProperty 4 | 5 | 6 | class ActedIn(StructuredRel): 7 | roles = ArrayProperty(StringProperty()) 8 | 9 | 10 | class Movie(DjangoNode): 11 | uuid = UniqueIdProperty(primary_key=True) 12 | title = StringProperty() 13 | tagline = StringProperty() 14 | released = IntegerProperty() 15 | 16 | directors = RelationshipFrom('Person', 'DIRECTED') 17 | writters = RelationshipFrom('Person', 'WROTE') 18 | producers = RelationshipFrom('Person', 'PRODUCED') 19 | reviewers = RelationshipFrom('Person', 'REVIEWED') 20 | actors = RelationshipFrom('Person', 'ACTED_IN', model=ActedIn) 21 | 22 | class Meta: 23 | app_label = 'movies' 24 | 25 | 26 | class Person(DjangoNode): 27 | uuid = UniqueIdProperty(primary_key=True) 28 | name = StringProperty() 29 | born = IntegerProperty() 30 | 31 | follows = RelationshipTo('Person', 'FOLLOWS') 32 | directed = RelationshipFrom('Movie', 'DIRECTED') 33 | wrote = RelationshipFrom('Movie', 'WROTE') 34 | produced = RelationshipFrom('Movie', 'PRODUCED') 35 | reviewed = RelationshipFrom('Movie', 'REVIEWED') 36 | acted_in = RelationshipFrom('Movie', 'ACTED_IN') 37 | 38 | class Meta: 39 | app_label = 'movies' 40 | 41 | -------------------------------------------------------------------------------- /movies/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Neo4j Movies 8 | 9 | 10 | 11 |
12 |
13 | 41 | 42 |
43 |
44 |
45 |
Search Results
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
MovieReleasedTagline
57 |
58 |
59 |
60 |
61 |
Details
62 |
63 |
64 | 65 |
66 |
67 |

Crew

68 |
    69 |
70 |
71 |
72 |
73 |
74 |
75 | 81 | 82 | 83 | 84 | 118 | 119 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /movies/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /movies/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.shortcuts import render 3 | from neomodel import Traversal 4 | from neomodel.sync_ import match 5 | from .models import Movie, Person 6 | 7 | 8 | def movies_index(request): 9 | movies = Movie.nodes.all() 10 | return render(request, "index.html", {"movies": movies}) 11 | 12 | 13 | def graph(request): 14 | nodes = [] 15 | rels = [] 16 | movies = Movie.nodes.has(actors=True) 17 | 18 | i = 0 19 | for movie in movies: 20 | nodes.append({"id": movie.element_id, "title": movie.title, "label": "movie"}) 21 | target = i 22 | i += 1 23 | 24 | for person in movie.actors: 25 | actor = {"id": person.element_id, "title": person.name, "label": "actor"} 26 | 27 | try: 28 | source = nodes.index(actor) 29 | except ValueError: 30 | nodes.append(actor) 31 | source = i 32 | i += 1 33 | rels.append({"source": source, "target": target}) 34 | 35 | return JsonResponse({"nodes": nodes, "links": rels}) 36 | 37 | 38 | def search(request): 39 | try: 40 | q = request.GET["q"] 41 | except KeyError: 42 | return JsonResponse([]) 43 | 44 | movies = Movie.nodes.filter(title__icontains=q) 45 | return JsonResponse( 46 | [ 47 | { 48 | "id": movie.element_id, 49 | "title": movie.title, 50 | "tagline": movie.tagline, 51 | "released": movie.released, 52 | "label": "movie", 53 | } 54 | for movie in movies 55 | ], 56 | safe=False, 57 | ) 58 | 59 | 60 | def serialize_cast(person, job, rel=None): 61 | return { 62 | "id": person.element_id, 63 | "name": person.name, 64 | "job": job, 65 | "role": rel.roles if rel else None, 66 | } 67 | 68 | 69 | def movie_by_title(request, title): 70 | movie = Movie.nodes.get(title=title) 71 | cast = [] 72 | 73 | for person in movie.directors: 74 | cast.append(serialize_cast(person, "directed")) 75 | 76 | for person in movie.writters: 77 | cast.append(serialize_cast(person, "wrote")) 78 | 79 | for person in movie.producers: 80 | cast.append(serialize_cast(person, "produced")) 81 | 82 | for person in movie.reviewers: 83 | cast.append(serialize_cast(person, "reviewed")) 84 | 85 | for person in movie.actors: 86 | rel = movie.actors.relationship(person) 87 | cast.append(serialize_cast(person, "acted", rel)) 88 | 89 | return JsonResponse( 90 | { 91 | "id": movie.element_id, 92 | "title": movie.title, 93 | "tagline": movie.tagline, 94 | "released": movie.released, 95 | "label": "movie", 96 | "cast": cast, 97 | } 98 | ) 99 | -------------------------------------------------------------------------------- /neomovies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-examples/neo4j-movies-python-neomodel/d98ddb9f5c7d95de85565ee13b9e963984897ac7/neomovies/__init__.py -------------------------------------------------------------------------------- /neomovies/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for neomovies project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'neomovies.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /neomovies/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for neomovies project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-fzxcj)f5&6h91sgvzc@0+y3wpwjtwi%khann94#wp-0a%*nz#6' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ["*"] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.auth', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | # Third party 43 | 'django_neomodel', 44 | 'movies' 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | "django.contrib.auth.middleware.AuthenticationMiddleware", 55 | ] 56 | 57 | ROOT_URLCONF = 'neomovies.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'neomovies.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 80 | 81 | #Connect to Neo4j Database 82 | NEOMODEL_NEO4J_BOLT_URL = os.environ.get('NEO4J_BOLT_URL', 'bolt://neo4j:bridges-canister-adviser@54.174.88.253:7687') 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'NAME': 'movies.db', 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'USER': '', 89 | 'PASSWORD': '', 90 | 'PORT': '', 91 | }, 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'en-us' 118 | 119 | TIME_ZONE = 'UTC' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 130 | 131 | STATIC_URL = '/static/' 132 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 133 | 134 | # Default primary key field type 135 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 136 | 137 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 138 | -------------------------------------------------------------------------------- /neomovies/urls.py: -------------------------------------------------------------------------------- 1 | """neomovies URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.urls import path 17 | from movies import views 18 | from django.contrib import admin 19 | 20 | urlpatterns = [ 21 | path('', views.movies_index), 22 | path('search', views.search), 23 | path('graph', views.graph), 24 | path('movie/', views.movie_by_title), 25 | path('admin/', admin.site.urls), 26 | ] 27 | -------------------------------------------------------------------------------- /neomovies/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for neomovies 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/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'neomovies.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gunicorn==20.1.0 2 | django_neomodel==0.2.0 3 | --------------------------------------------------------------------------------