initial application skeleton

This commit is contained in:
Frédéric Péters 2016-02-13 09:56:55 +01:00
parent b77dea31a9
commit d13dbf8178
18 changed files with 617 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.pyc
local_settings.py
/db.sqlite3
/dist
/chrono.egg-info

11
MANIFEST.in Normal file
View File

@ -0,0 +1,11 @@
# locales
recursive-include chrono/locale *.po *.mo
# static
# templates
recursive-include combo/manager/templates *.html
include COPYING README
include MANIFEST.in
include VERSION

0
chrono/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,22 @@
{% extends "gadjo/base.html" %}
{% load staticfiles i18n %}
{% block page-title %}
{% trans 'Agendas' as default_site_title %}
{% firstof site_title default_site_title %}
{% endblock %}
{% block site-title %}
{% trans 'Agendas' as default_site_title %}
{% firstof site_title default_site_title %}
{% endblock %}
{% block footer %}Chrono — Copyright © Entr'ouvert{% endblock %}
{% block homepage-url %}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-homepage' %}">{% trans 'Agendas' %}</a>
{% endblock %}
{% block logout-url %}{% url 'auth_logout' %}{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "chrono/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans 'Agendas' %}</h2>
{% endblock %}
{% block content %}
{% if object_list %}
<div class="objects-list">
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This site doesn't have any agenda yet. Click on the "New" button in the top
right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "chrono/manager_base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans 'Log in' %}" />
</form>
{% endblock %}

24
chrono/manager/urls.py Normal file
View File

@ -0,0 +1,24 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import patterns, url
from . import views
urlpatterns = patterns('chrono.views',
url(r'^$', views.homepage, name='chrono-manager-homepage'),
url(r'^menu.json$', views.menu_json),
)

44
chrono/manager/views.py Normal file
View File

@ -0,0 +1,44 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.views.generic import TemplateView
class HomepageView(TemplateView):
template_name = 'chrono/manager_home.html'
homepage = HomepageView.as_view()
def menu_json(request):
response = HttpResponse(content_type='application/json')
label = _('Agendas')
json_str = json.dumps([{'label': force_text(label),
'slug': 'calendar',
'url': request.build_absolute_uri(reverse('chrono-manager-homepage'))
}])
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:
json_str = '%s(%s);' % (request.GET[variable], json_str)
break
response.write(json_str)
return response

146
chrono/settings.py Normal file
View File

@ -0,0 +1,146 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Django settings file; it loads the default settings, and local settings
(from a local_settings.py file, or a configuration file set in the
COMBO_SETTINGS_FILE environment variable).
The local settings file should exist, at least to set a suitable SECRET_KEY,
and to disable DEBUG mode in production.
"""
import os
from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS, STATICFILES_FINDERS
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '1am-@xw1d%#+1f+4$ws4e3*k9+z&f4f9i#di4pt4@_%829(%bl'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'gadjo',
'chrono.manager',
)
MIDDLEWARE_CLASSES = (
'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',
)
TEMPLATE_CONTEXT_PROCESSORS += (
'django.core.context_processors.request',
)
# Serve xstatic files, required for gadjo
STATICFILES_FINDERS = tuple(STATICFILES_FINDERS) + ('gadjo.finders.XStaticFinder',)
ROOT_URLCONF = 'chrono.urls'
WSGI_APPLICATION = 'chrono.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (os.path.join(BASE_DIR, 'chrono', 'locale'), )
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# Authentication settings
try:
import mellon
except ImportError:
mellon = None
if mellon is not None:
AUTHENTICATION_BACKENDS = (
'mellon.backends.SAMLBackend',
'django.contrib.auth.backends.ModelBackend',
)
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_URL = '/logout/'
MELLON_ATTRIBUTE_MAPPING = {
'email': '{attributes[email][0]}',
'first_name': '{attributes[first_name][0]}',
'last_name': '{attributes[last_name][0]}',
}
MELLON_SUPERUSER_MAPPING = {
'is_superuser': 'true',
}
MELLON_USERNAME_TEMPLATE = '{attributes[name_id_content]}'
MELLON_IDENTITY_PROVIDERS = []
local_settings_file = os.environ.get('CHRONO_SETTINGS_FILE',
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
if os.path.exists(local_settings_file):
execfile(local_settings_file)

42
chrono/urls.py Normal file
View File

@ -0,0 +1,42 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.conf.urls import patterns, include, url
from .urls_utils import decorated_includes, manager_required
from .views import homepage, login, logout
from .manager.urls import urlpatterns as chrono_manager_urls
urlpatterns = patterns('',
url(r'^$', homepage, name='home'),
url(r'^manage/', decorated_includes(manager_required,
include(chrono_manager_urls))),
url(r'^logout/$', logout, name='auth_logout'),
url(r'^login/$', login, name='auth_login'),
)
if 'mellon' in settings.INSTALLED_APPS:
urlpatterns += patterns('', url(r'^accounts/mellon/', include('mellon.urls')))
# static and media files
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

62
chrono/urls_utils.py Normal file
View File

@ -0,0 +1,62 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Decorating URL includes, <https://djangosnippets.org/snippets/2532/>
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
class DecoratedURLPattern(RegexURLPattern):
def resolve(self, *args, **kwargs):
result = super(DecoratedURLPattern, self).resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
class DecoratedRegexURLResolver(RegexURLResolver):
def resolve(self, *args, **kwargs):
result = super(DecoratedRegexURLResolver, self).resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes
for item in urlconf_module:
if isinstance(item, RegexURLPattern):
item.__class__ = DecoratedURLPattern
item._decorate_with = func
elif isinstance(item, RegexURLResolver):
item.__class__ = DecoratedRegexURLResolver
item._decorate_with = func
return urlconf_module, app_name, namespace
def manager_required(function=None, login_url=None):
def check_manager(user):
if user and user.is_staff:
return True
if user and not user.is_anonymous():
raise PermissionDenied()
# As the last resort, show the login form
return False
actual_decorator = user_passes_test(check_manager, login_url=login_url)
if function:
return actual_decorator(function)
return actual_decorator

49
chrono/views.py Normal file
View File

@ -0,0 +1,49 @@
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urllib
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url
try:
from mellon.utils import get_idps
except ImportError:
get_idps = lambda: []
def login(request, *args, **kwargs):
if any(get_idps()):
if not 'next' in request.GET:
return HttpResponseRedirect(resolve_url('mellon_login'))
return HttpResponseRedirect(resolve_url('mellon_login') + '?next='
+ urllib.quote(request.GET.get('next')))
return auth_views.login(request, *args, **kwargs)
def logout(request, next_page=None):
if any(get_idps()):
return HttpResponseRedirect(resolve_url('mellon_logout'))
auth_logout(request)
if next_page is not None:
next_page = resolve_url(next_page)
else:
next_page = '/'
return HttpResponseRedirect(next_page)
def homepage(request, *args, **kwargs):
return HttpResponseRedirect(resolve_url('chrono-manager-homepage'))

14
chrono/wsgi.py Normal file
View File

@ -0,0 +1,14 @@
"""
WSGI config for chrono 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/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

10
manage.py Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

115
setup.py Normal file
View File

@ -0,0 +1,115 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import glob
import os
import re
import subprocess
import sys
from setuptools.command.install_lib import install_lib as _install_lib
from distutils.command.build import build as _build
from distutils.command.sdist import sdist
from distutils.cmd import Command
from setuptools import setup, find_packages
class eo_sdist(sdist):
def run(self):
if os.path.exists('VERSION'):
os.remove('VERSION')
version = get_version()
version_file = open('VERSION', 'w')
version_file.write(version)
version_file.close()
sdist.run(self)
if os.path.exists('VERSION'):
os.remove('VERSION')
def get_version():
if os.path.exists('VERSION'):
version_file = open('VERSION', 'r')
version = version_file.read()
version_file.close()
return version
if os.path.exists('.git'):
p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE)
result = p.communicate()[0]
if p.returncode == 0:
version = result.split()[0][1:]
version = version.replace('-', '.')
return version
return '0'
def data_tree(destdir, sourcedir):
extensions = ['.css', '.png', '.jpeg', '.jpg', '.gif', '.xml', '.html', '.js']
r = []
for root, dirs, files in os.walk(sourcedir):
l = [os.path.join(root, x) for x in files if os.path.splitext(x)[1] in extensions]
r.append((root.replace(sourcedir, destdir, 1), l))
return r
class compile_translations(Command):
description = 'compile message catalogs to MO files via django compilemessages'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
from django.core.management import call_command
for path, dirs, files in os.walk('chrono'):
if 'locale' not in dirs:
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
except ImportError:
sys.stderr.write('!!! Please install Django >= 1.4 to build translations\n')
class build(_build):
sub_commands = [('compile_translations', None)] + _build.sub_commands
class install_lib(_install_lib):
def run(self):
self.run_command('compile_translations')
_install_lib.run(self)
setup(
name='chrono',
version=get_version(),
description='Agendas System',
author='Frederic Peters',
author_email='fpeters@entrouvert.com',
packages=find_packages(),
include_package_data=True,
scripts=('manage.py',),
url='https://dev.entrouvert.org/projects/chrono/',
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
],
install_requires=['django>=1.7, <1.8',
'gadjo',
],
zip_safe=False,
cmdclass={
'build': build,
'compile_translations': compile_translations,
'install_lib': install_lib,
'sdist': eo_sdist,
},
)

1
tests/settings.py Normal file
View File

@ -0,0 +1 @@
LANGUAGE_CODE = 'en-us'

40
tests/test_manager.py Normal file
View File

@ -0,0 +1,40 @@
from django.contrib.auth.models import User
import pytest
from webtest import TestApp
from chrono.wsgi import application
pytestmark = pytest.mark.django_db
@pytest.fixture
def admin_user():
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
user = User.objects.create_superuser('admin', email=None, password='admin')
return user
def login(app, username='admin', password='admin'):
login_page = app.get('/login/')
login_form = login_page.forms[0]
login_form['username'] = username
login_form['password'] = password
resp = login_form.submit()
assert resp.status_int == 302
return app
def test_unlogged_access():
# connect while not being logged in
app = TestApp(application)
assert app.get('/manage/', status=302).location == 'http://localhost:80/login/?next=/manage/'
def test_access(admin_user):
app = login(TestApp(application))
resp = app.get('/manage/', status=200)
assert '<h2>Agendas</h2>' in resp.body
assert "This site doesn't have any agenda yet." in resp.body
def test_logout(admin_user):
app = login(TestApp(application))
app.get('/logout/')
assert app.get('/manage/', status=302).location == 'http://localhost:80/login/?next=/manage/'