diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..19720b7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +python_files = test*.py +addopts = --ff -v --nomigrations --doctest-modules tests/ +DJANGO_SETTINGS_MODULE = fision.settings diff --git a/setup.py b/setup.py index 5ca4cb6..de7b33b 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,9 @@ 'channels>=2.2.0,<2.3', ], extras_require={ - 'development': [ + 'test': [ + 'pytest-django', + 'pytest-asyncio', 'rjsmin', ], }, diff --git a/tests/fision/__init__.py b/tests/fision/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fision/asgi.py b/tests/fision/asgi.py new file mode 100644 index 0000000..cdfa2f2 --- /dev/null +++ b/tests/fision/asgi.py @@ -0,0 +1,10 @@ +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter + +from reactor.urls import websocket_urlpatterns + +application = ProtocolTypeRouter({ + 'websocket': AuthMiddlewareStack(URLRouter( + websocket_urlpatterns, + )) +}) diff --git a/tests/fision/settings.py b/tests/fision/settings.py new file mode 100644 index 0000000..aefad00 --- /dev/null +++ b/tests/fision/settings.py @@ -0,0 +1,158 @@ +""" +Django settings for fision project. + +Generated by 'django-admin startproject' using Django 2.2. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os +up = os.path.dirname + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = up(up(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'b7!j@8gk-vdq3tona^(i(qg#xiir*%r-@u1f&fw@@(ccwy^ijb' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'fision.todo', + 'reactor', + + 'channels', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'fision.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'fision.wsgi.application' +ASGI_APPLICATION = 'fision.asgi.application' + +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels.layers.InMemoryChannelLayer', + # 'CONFIG': { + # "hosts": [('127.0.0.1', 6379)], + # }, + }, +} + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ATOMIC_REQUESTS': True, + 'OPTIONS': { + 'timeout': 20, + }, + "TEST": { + "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"), + }, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.2/howto/static-files/ + +STATIC_URL = '/static/' + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'reactor': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + }, +} diff --git a/tests/fision/todo/__init__.py b/tests/fision/todo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fision/todo/admin.py b/tests/fision/todo/admin.py new file mode 100644 index 0000000..1e756fa --- /dev/null +++ b/tests/fision/todo/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from . import models + + +@admin.register(models.Item) +class ItemAdmin(admin.ModelAdmin): + pass diff --git a/tests/fision/todo/apps.py b/tests/fision/todo/apps.py new file mode 100644 index 0000000..6c85b8d --- /dev/null +++ b/tests/fision/todo/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TodoConfig(AppConfig): + name = 'todo' diff --git a/tests/fision/todo/migrations/0001_initial.py b/tests/fision/todo/migrations/0001_initial.py new file mode 100644 index 0000000..57329f9 --- /dev/null +++ b/tests/fision/todo/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2 on 2019-04-17 21:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('completed', models.BooleanField(default=False)), + ('text', models.CharField(max_length=256)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/tests/fision/todo/migrations/__init__.py b/tests/fision/todo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fision/todo/models.py b/tests/fision/todo/models.py new file mode 100644 index 0000000..1825e0f --- /dev/null +++ b/tests/fision/todo/models.py @@ -0,0 +1,59 @@ +from uuid import uuid4 +from django.db import models +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver + +from reactor.component import send_to_group + + +class BaseModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + + class Meta: + abstract = True + + +class ItemQS(models.QuerySet): + + @property + def completed(self): + return self.filter(completed=True) + + @property + def active(self): + return self.filter(completed=False) + + def update(self, *args, **kwargs): + results = super().update(*args, **kwargs) + send_to_group('item', 'update') + send_to_group('item.updated', 'update') + for item in self: + send_to_group(f'item.{item.id}', 'update') + return results + + +class Item(BaseModel): + completed = models.BooleanField(default=False) + text = models.CharField(max_length=256) + + objects = ItemQS.as_manager() + + def __str__(self): + return self.text + + +@receiver(post_save, sender=Item) +def emit_element_saved(sender, instance, created, **kwargs): + send_to_group(f'item', 'update') + if created: + send_to_group('item.new', 'update') + else: + send_to_group('item.updated', 'update') + send_to_group(f'item.{instance.id}', 'update') + + +@receiver(post_delete, sender=Item) +def emit_element_deleted(sender, instance, **kwargs): + send_to_group('item', 'update') + send_to_group('item.deleted', 'update') + send_to_group(f'item.{instance.id}', 'update') diff --git a/tests/fision/todo/static/todo.css b/tests/fision/todo/static/todo.css new file mode 100644 index 0000000..13a1772 --- /dev/null +++ b/tests/fision/todo/static/todo.css @@ -0,0 +1,379 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/tests/fision/todo/templates/todo.html b/tests/fision/todo/templates/todo.html new file mode 100644 index 0000000..46e9e6f --- /dev/null +++ b/tests/fision/todo/templates/todo.html @@ -0,0 +1,29 @@ +{% load static reactor %} + + + + + + Template • TodoMVC + + + {% reactor_header %} + + + + {% component 'x-todo-list' %} + + + + + + + + diff --git a/tests/fision/todo/templates/todo/counter.html b/tests/fision/todo/templates/todo/counter.html new file mode 100644 index 0000000..84931e6 --- /dev/null +++ b/tests/fision/todo/templates/todo/counter.html @@ -0,0 +1,7 @@ +{% load reactor %} + + + {% with amount=this.items.active.count %} + {{ amount }} item{{ amount|pluralize }} left + {% endwith %} + diff --git a/tests/fision/todo/templates/todo/item.html b/tests/fision/todo/templates/todo/item.html new file mode 100644 index 0000000..db9983b --- /dev/null +++ b/tests/fision/todo/templates/todo/item.html @@ -0,0 +1,29 @@ +{% load reactor %} + +
  • +
    + + + +
    + +
  • +
    diff --git a/tests/fision/todo/templates/todo/list.html b/tests/fision/todo/templates/todo/list.html new file mode 100644 index 0000000..fb4a5b7 --- /dev/null +++ b/tests/fision/todo/templates/todo/list.html @@ -0,0 +1,73 @@ +{% load reactor %} + +
    +
    +

    todos

    + +
    + +
    + {% if this.items %} + + +
      + + + {% for item in this.items %} + {% component 'x-todo-item' id=item.id showing=this.showing %} + {% endfor %} +
    + {% endif %} +
    + + {% if this.items %} + + {% endif %} + + diff --git a/tests/fision/todo/tests.py b/tests/fision/todo/tests.py new file mode 100644 index 0000000..224f357 --- /dev/null +++ b/tests/fision/todo/tests.py @@ -0,0 +1,92 @@ +import websocket +import json +from django.test import TestCase, Client + +from channels.testing import ChannelsLiveServerTestCase + +from .models import Item + + +class TestNormalRendering(TestCase): + + def setUp(self): + Item.objects.create(text='First task') + Item.objects.create(text='Second task') + self.c = Client() + + def test_everything_two_tasks_are_rendered(self): + response = self.c.get('/') + assert response.status_code == 200 + self.assertContains(response, 'First task') + self.assertContains(response, 'Second task') + + +class LiveTesting(ChannelsLiveServerTestCase): + def test_how_it_works(self): + assert Item.objects.count() == 0 + host = self.live_server_url[len('http://'):] + ws = websocket.WebSocket() + ws.connect(f'ws://{host}/reactor') + ws.send(json.dumps({ + 'command': 'join', + 'payload': { + 'tag_name': 'x-todo-list', + 'state': {'id': 'someid', 'showing': 'all'}, + 'echo_render': True, + } + })) + response = json.loads(ws.recv()) + assert response['type'] == 'render' + assert response['id'] == 'someid' + assert 'html' in response + assert 'left' not in response['html'] + + ws.send(json.dumps({ + 'command': 'user_event', + 'payload': { + 'tag_name': 'x-todo-list', + 'name': 'add', + 'state': {'id': 'someid', 'new_item': 'First task'}, + } + })) + response = json.loads(ws.recv()) + task = Item.objects.first() # type: Item + assert task + assert not task.completed + assert response['type'] == 'render' + assert response['id'] == 'someid' + assert 'First task' in response['html'] + assert 'checked' not in response['html'] + task_state = { + 'id': str(task.id), + 'editing': False, + 'showing': 'all', + } + assert ( + json.dumps(task_state).replace('"', '"') in response['html'] + ) + + ws.send(json.dumps({ + 'command': 'join', + 'payload': { + 'tag_name': 'x-todo-item', + 'state': task_state, + } + })) + + # Mark task as completed + ws.send(json.dumps({ + 'command': 'user_event', + 'payload': { + 'tag_name': 'x-todo-item', + 'name': 'completed', + 'state': dict(task_state, completed=True), + } + })) + + response = json.loads(ws.recv()) + task.refresh_from_db() + assert task.completed + assert response['type'] == 'render' + assert response['id'] == str(task.id) + assert 'checked' in response['html'] diff --git a/tests/fision/todo/urls.py b/tests/fision/todo/urls.py new file mode 100644 index 0000000..d81e90b --- /dev/null +++ b/tests/fision/todo/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.todo) +] diff --git a/tests/fision/todo/views.py b/tests/fision/todo/views.py new file mode 100644 index 0000000..09399f8 --- /dev/null +++ b/tests/fision/todo/views.py @@ -0,0 +1,93 @@ +from django.shortcuts import render +from reactor.component import Component + +from .models import Item + + +def todo(request): + return render(request, 'todo.html') + + +class XTodoList(Component): + template_name = 'todo/list.html' + + def mount(self, showing='all', **kwargs): + self.showing = showing + self.subscriptions.add('item.new') + + def serialize(self): + return dict( + id=self.id, + showing=self.showing, + ) + + @property + def items(self): + return Item.objects.all() + + @property + def all_items_are_completed(self): + return self.items.count() == self.items.completed.count() + + def receive_add(self, new_item, **kwargs): + Item.objects.create(text=new_item) + + def receive_show(self, showing, **kwargs): + self.showing = showing + + def receive_toggle_all(self, toggle_all, **kwargs): + self.items.update(completed=toggle_all) + + def receive_clear_completed(self, **kwargs): + self.items.completed.delete() + + +class XTodoCounter(Component): + template_name = 'todo/counter.html' + + def mount(self, items=None, *args, **kwargs): + self.items = items or Item.objects.all() + self.subscriptions.add('item') + + +class XTodoItem(Component): + template_name = 'todo/item.html' + + def mount(self, item=None, editing=False, showing='all', **kwargs): + self.editing = editing + self.showing = showing + self.item = item or Item.objects.filter(id=self.id).first() + if self.item: + self.subscriptions.add(f'item.{self.item.id}') + else: + self.send_destroy() + + def serialize(self): + return dict( + id=self.id, + editing=self.editing, + showing=self.showing, + ) + + def is_visible(self): + return ( + self.showing == 'all' or + self.showing == 'completed' and self.item.completed or + self.showing == 'active' and not self.item.completed + ) + + def receive_destroy(self, **kwargs): + self.item.delete() + + def receive_completed(self, completed, **kwargs): + self.item.completed = completed + self.item.save() + + def receive_toggle_editing(self, **kwargs): + if not self.item.completed: + self.editing = not self.editing + + def receive_save(self, text, **kwargs): + self.item.text = text + self.item.save() + self.editing = False diff --git a/tests/fision/urls.py b/tests/fision/urls.py new file mode 100644 index 0000000..7f2169c --- /dev/null +++ b/tests/fision/urls.py @@ -0,0 +1,23 @@ +"""fision URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('', include('fision.todo.urls')), + # path('', include('fision.frontend.urls')), + path('admin/', admin.site.urls), +] diff --git a/tests/fision/wsgi.py b/tests/fision/wsgi.py new file mode 100644 index 0000000..b7c74e4 --- /dev/null +++ b/tests/fision/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for fision project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fision.settings') + +application = get_wsgi_application() diff --git a/tests/manage.py b/tests/manage.py new file mode 100755 index 0000000..c800716 --- /dev/null +++ b/tests/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fision.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main()