diff --git a/.travis.yml b/.travis.yml index 159d731e..6db83cc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ install: script: - flake8 +- coverage run --source=junction --omit='*tests*,*commands*,*migrations*,*admin*,*wsgi*' -m py.test -v --tb=native +- coverage report notifications: email: diff --git a/junction/junction/__init__.py b/junction/__init__.py similarity index 100% rename from junction/junction/__init__.py rename to junction/__init__.py diff --git a/junction/conferences/admin.py b/junction/conferences/admin.py index c903bfef..68578996 100644 --- a/junction/conferences/admin.py +++ b/junction/conferences/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from conferences.models import Conference, ConferenceModerator, \ - ConferenceProposalReviewer -from custom_utils.admin import AuditAdmin +from junction.custom_utils.admin import AuditAdmin + +from . import models class ConferenceAdmin(AuditAdmin): @@ -17,6 +17,6 @@ class ConferenceProposallReviewerAdmin(AuditAdmin): list_display = ('conference', 'reviewer', 'active') + AuditAdmin.list_display -admin.site.register(Conference, ConferenceAdmin) -admin.site.register(ConferenceModerator, ConferenceModeratorAdmin) -admin.site.register(ConferenceProposalReviewer, ConferenceProposallReviewerAdmin) +admin.site.register(models.Conference, ConferenceAdmin) +admin.site.register(models.ConferenceModerator, ConferenceModeratorAdmin) +admin.site.register(models.ConferenceProposalReviewer, ConferenceProposallReviewerAdmin) diff --git a/junction/conferences/models.py b/junction/conferences/models.py index 6045b6cd..cb83daaf 100644 --- a/junction/conferences/models.py +++ b/junction/conferences/models.py @@ -5,8 +5,8 @@ from django.db import models from django_extensions.db.fields import AutoSlugField -from custom_utils.constants import CONFERENCE_STATUS_LIST -from custom_utils.models import AuditModel +from junction.custom_utils.constants import CONFERENCE_STATUS_LIST +from junction.custom_utils.models import AuditModel class Conference(AuditModel): diff --git a/junction/junction/dev.py b/junction/junction/dev.py deleted file mode 100644 index 7685b09d..00000000 --- a/junction/junction/dev.py +++ /dev/null @@ -1,13 +0,0 @@ -import os - - -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/junction/pages/views.py b/junction/pages/views.py index 116553e0..28213435 100644 --- a/junction/pages/views.py +++ b/junction/pages/views.py @@ -2,7 +2,7 @@ # Third Party Stuff from django.views.generic import TemplateView -from conferences.models import Conference +from junction.conferences.models import Conference class HomePageView(TemplateView): diff --git a/junction/proposals/admin.py b/junction/proposals/admin.py index 0b8576ae..ec2b959c 100644 --- a/junction/proposals/admin.py +++ b/junction/proposals/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from custom_utils.admin import AuditAdmin, TimeAuditAdmin -from proposals.models import ( +from junction.custom_utils.admin import AuditAdmin, TimeAuditAdmin +from junction.proposals.models import ( Proposal, ProposalComment, ProposalCommentVote, diff --git a/junction/proposals/comment_urls.py b/junction/proposals/comment_urls.py index b2b50157..705510a8 100644 --- a/junction/proposals/comment_urls.py +++ b/junction/proposals/comment_urls.py @@ -1,6 +1,6 @@ from django.conf.urls import patterns, url -from proposals.views import create_proposal_comment +from .views import create_proposal_comment urlpatterns = patterns( diff --git a/junction/proposals/forms.py b/junction/proposals/forms.py index ba6d6d4c..ee731971 100644 --- a/junction/proposals/forms.py +++ b/junction/proposals/forms.py @@ -2,8 +2,8 @@ from django.utils.safestring import mark_safe from pagedown.widgets import PagedownWidget -from custom_utils.constants import PROPOSAL_TARGET_AUDIENCES, PROPOSAL_STATUS_LIST -from proposals.models import ProposalSection, ProposalType +from junction.custom_utils.constants import PROPOSAL_TARGET_AUDIENCES, PROPOSAL_STATUS_LIST +from junction.proposals.models import ProposalSection, ProposalType def _get_proposal_section_choices(conference): diff --git a/junction/proposals/models.py b/junction/proposals/models.py index e8c6637f..3cbcb475 100644 --- a/junction/proposals/models.py +++ b/junction/proposals/models.py @@ -1,15 +1,21 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +# Third Party Stuff from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import models + from django_extensions.db.fields import AutoSlugField -from conferences.models import Conference -from custom_utils.constants import PROPOSAL_USER_VOTE_ROLES, PROPOSAL_STATUS_LIST, \ - PROPOSAL_REVIEW_STATUS_LIST, PROPOSAL_TARGET_AUDIENCES -from custom_utils.models import AuditModel, TimeAuditModel +from junction.conferences.models import Conference +from junction.custom_utils.constants import ( + PROPOSAL_REVIEW_STATUS_LIST, + PROPOSAL_STATUS_LIST, + PROPOSAL_TARGET_AUDIENCES, + PROPOSAL_USER_VOTE_ROLES +) +from junction.custom_utils.models import AuditModel, TimeAuditModel class ProposalSection(AuditModel): diff --git a/junction/proposals/proposal_urls.py b/junction/proposals/proposal_urls.py index e31841be..6c209e0a 100644 --- a/junction/proposals/proposal_urls.py +++ b/junction/proposals/proposal_urls.py @@ -1,7 +1,15 @@ -from django.conf.urls import patterns, url +# -*- coding: utf-8 -*- +from __future__ import unicode_literals -from proposals.views import create_proposal, list_proposals, update_proposal, detail_proposal, delete_proposal +from django.conf.urls import patterns, url +from .views import ( + create_proposal, + delete_proposal, + detail_proposal, + list_proposals, + update_proposal +) urlpatterns = patterns( '', diff --git a/junction/proposals/views.py b/junction/proposals/views.py index 8c28f7ad..268a676a 100644 --- a/junction/proposals/views.py +++ b/junction/proposals/views.py @@ -1,13 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# Third Party Stuff from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.http.response import HttpResponseForbidden, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.views.decorators.http import require_http_methods -from conferences.models import Conference, ConferenceProposalReviewer +from junction.conferences.models import Conference, ConferenceProposalReviewer -from proposals.forms import ProposalCommentForm, ProposalForm, ProposalVoteForm -from proposals.models import Proposal, ProposalComment, ProposalVote +from .forms import ProposalCommentForm, ProposalForm, ProposalVoteForm +from .models import Proposal, ProposalComment, ProposalVote def _is_proposal_author(user, proposal): diff --git a/junction/proposals/vote_urls.py b/junction/proposals/vote_urls.py index e3cde456..4f7a7311 100644 --- a/junction/proposals/vote_urls.py +++ b/junction/proposals/vote_urls.py @@ -1,6 +1,6 @@ from django.conf.urls import patterns, url -from proposals.views import proposal_vote_up, proposal_vote_down +from .views import proposal_vote_up, proposal_vote_down urlpatterns = patterns( diff --git a/junction/urls.py b/junction/urls.py index ad3c0500..0dde9b7b 100644 --- a/junction/urls.py +++ b/junction/urls.py @@ -23,9 +23,9 @@ url('^markdown/', include('django_markdown.urls')), # Proposals related - url(r'^(?P[\w-]+)/proposals/', include('proposals.proposal_urls')), - url(r'^(?P[\w-]+)/proposal-comments/', include('proposals.comment_urls')), - url(r'^(?P[\w-]+)/proposal-votes/', include('proposals.vote_urls')), + url(r'^(?P[\w-]+)/proposals/', include('junction.proposals.proposal_urls')), + url(r'^(?P[\w-]+)/proposal-comments/', include('junction.proposals.comment_urls')), + url(r'^(?P[\w-]+)/proposal-votes/', include('junction.proposals.vote_urls')), # Static Pages. TODO: to be refactored url(r'^speakers/$', TemplateView.as_view(template_name='static-content/speakers.html',), name='speakers-static'), @@ -42,7 +42,7 @@ name='conference-detail'), # add at the last for minor performance gain - url(r'^', include('pages.urls', namespace='pages')), + url(r'^', include('junction.pages.urls', namespace='pages')), ) if settings.DEBUG: diff --git a/junction/manage.py b/manage.py old mode 100644 new mode 100755 similarity index 70% rename from junction/manage.py rename to manage.py index 2de46dac..f9726f9e --- a/junction/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "junction.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..3c0f973a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = settings +python_paths = . +norecursedirs = .tox .git */migrations/* */static/* docs venv diff --git a/requirements-dev.txt b/requirements-dev.txt index 2cd4fdf4..d2ac4e5d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,12 @@ # Testing # ------------------------------------------------- +mock==1.0.1 +factory_boy==2.4.1 flake8==2.2.5 +pytest-django==2.7.0 +pytest-flakes==0.2 +pytest-pythonpath==0.3 +pytest-ipdb==0.1-prerelease2 + +coverage==3.7.1 diff --git a/settings/__init__.py b/settings/__init__.py new file mode 100644 index 00000000..61d72faa --- /dev/null +++ b/settings/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function + +# Standard Library +import sys + +if "test" in sys.argv: + print("\033[1;91mNo django tests.\033[0m") + print("Try: \033[1;33mpy.test\033[0m") + sys.exit(0) + +from .common import * # noqa + +try: + from .dev import * # noqa + from .prod import * # noqa +except ImportError: + pass diff --git a/junction/junction/settings.py b/settings/common.py similarity index 89% rename from junction/junction/settings.py rename to settings/common.py index cbfbfb4e..0da1f730 100644 --- a/junction/junction/settings.py +++ b/settings/common.py @@ -2,7 +2,12 @@ from django.conf.global_settings import * # noqa -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +# Standard Library +from os.path import dirname, join + +# Build paths inside the project like this: os.path.join(ROOT_DIR, ...) +ROOT_DIR = dirname(dirname(__file__)) +APP_DIR = join(ROOT_DIR, 'junction') SITE_ID = 1 @@ -44,9 +49,9 @@ ) OUR_APPS = ( - 'conferences', - 'proposals', - 'pages', + 'junction.conferences', + 'junction.proposals', + 'junction.pages', ) INSTALLED_APPS = CORE_APPS + THIRD_PARTY_APPS + OUR_APPS @@ -125,21 +130,21 @@ } -ROOT_URLCONF = 'urls' -WSGI_APPLICATION = 'junction.wsgi.application' +ROOT_URLCONF = 'junction.urls' +WSGI_APPLICATION = 'wsgi.application' TIME_ZONE = 'Asia/Kolkata' USE_L10N = True USE_TZ = True STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'assets', 'collected-static') +STATIC_ROOT = os.path.join(APP_DIR, 'assets', 'collected-static') STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'assets', 'static'), + os.path.join(APP_DIR, 'assets', 'static'), ) TEMPLATE_DIRS = ( - os.path.join(BASE_DIR, 'templates'), + os.path.join(APP_DIR, 'templates'), ) DATABASES = { @@ -165,11 +170,3 @@ DEBUG = TEMPLATE_DEBUG = os.environ.get('DEBUG', 'on') == 'on' ALLOWED_HOSTS = [] # TODO: - -# Dev Settings - -try: - from junction.dev import * # noqa - from junction.prod import * # noqa -except ImportError: - pass diff --git a/junction/junction/dev.py.sample b/settings/dev.py.sample similarity index 100% rename from junction/junction/dev.py.sample rename to settings/dev.py.sample diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..7a34e273 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Standard Library +import os + +# Third Party Stuff +import django +# import pytest + +from .fixtures import * # noqa + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + + +def pytest_configure(config): + django.setup() diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..86ca19da --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Standard Library +import functools + +# Third Party Stuff +import mock +import pytest + + +class Object: + pass + + +@pytest.fixture +def object(): + return Object() + + +class PartialMethodCaller: + def __init__(self, obj, **partial_params): + self.obj = obj + self.partial_params = partial_params + + def __getattr__(self, name): + return functools.partial(getattr(self.obj, name), **self.partial_params) + + +@pytest.fixture +def client(): + from django.test.client import Client + + class _Client(Client): + def login(self, user=None, backend="django.contrib.auth.backends.ModelBackend", **credentials): + if user is None: + return super(_Client, self).login(**credentials) + + with mock.patch('django.contrib.auth.authenticate') as authenticate: + user.backend = backend + authenticate.return_value = user + return super(_Client, self).login(**credentials) + + @property + def json(self): + return PartialMethodCaller(obj=self, content_type='application/json;charset="utf-8"') + + return _Client() + + +@pytest.fixture +def outbox(): + from django.core import mail + + return mail.outbox diff --git a/tests/integrations/__init__.py b/tests/integrations/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/integrations/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/integrations/test_permissions.py b/tests/integrations/test_permissions.py new file mode 100644 index 00000000..f162baf0 --- /dev/null +++ b/tests/integrations/test_permissions.py @@ -0,0 +1,17 @@ +import pytest +from django.core.urlresolvers import reverse + + +pytestmark = pytest.mark.django_db + + +def test_public_urls(client): + + public_urls = [ + reverse('pages:homepage'), + '/nimda/login/', + ] + + for url in public_urls: + response = client.get(url) + assert response.status_code == 200 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..53a1bde7 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Standard Library +import functools + +# Third Party Stuff +from django.conf import settings +from django.db.models import signals + + +def signals_switch(): + pre_save = signals.pre_save.receivers + post_save = signals.post_save.receivers + + def disconnect(): + signals.pre_save.receivers = [] + signals.post_save.receivers = [] + + def reconnect(): + signals.pre_save.receivers = pre_save + signals.post_save.receivers = post_save + + return disconnect, reconnect + + +disconnect_signals, reconnect_signals = signals_switch() + + +def set_settings(**new_settings): + """Decorator for set django settings that will be only available during the + wrapped-function execution. + + For example: + @set_settings(FOO='bar') + def myfunc(): + ... + + @set_settings(FOO='bar') + class TestCase: + ... + """ + def decorator(testcase): + if type(testcase) is type: + namespace = { + "OVERRIDE_SETTINGS": new_settings, "ORIGINAL_SETTINGS": {}} + wrapper = type(testcase.__name__, (SettingsTestCase, testcase), + namespace) + else: + @functools.wraps(testcase) + def wrapper(*args, **kwargs): + old_settings = override_settings(new_settings) + try: + testcase(*args, **kwargs) + finally: + override_settings(old_settings) + + return wrapper + + return decorator + + +def override_settings(new_settings): + old_settings = {} + for name, new_value in new_settings.items(): + old_settings[name] = getattr(settings, name, None) + setattr(settings, name, new_value) + return old_settings + + +class SettingsTestCase(object): + @classmethod + def setup_class(cls): + cls.ORIGINAL_SETTINGS = override_settings(cls.OVERRIDE_SETTINGS) + + @classmethod + def teardown_class(cls): + override_settings(cls.ORIGINAL_SETTINGS) + cls.OVERRIDE_SETTINGS.clear() diff --git a/junction/junction/wsgi.py b/wsgi.py similarity index 82% rename from junction/junction/wsgi.py rename to wsgi.py index d6079cc9..1cc38086 100644 --- a/junction/junction/wsgi.py +++ b/wsgi.py @@ -8,7 +8,7 @@ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "junction.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.wsgi import get_wsgi_application application = get_wsgi_application()