From: Łukasz Rekucki Date: Sat, 17 Apr 2010 21:59:47 +0000 (+0200) Subject: Profile edit and password change views. Better login/logout templates. Some polish... X-Git-Url: https://git.mdrn.pl/cas.git/commitdiff_plain/37e278e43ece42375352c07d9f04991ef58d992c?ds=inline Profile edit and password change views. Better login/logout templates. Some polish translations. --- diff --git a/provider/cas_provider/models.py b/provider/cas_provider/models.py index 5d09912..6e7ebaf 100644 --- a/provider/cas_provider/models.py +++ b/provider/cas_provider/models.py @@ -10,14 +10,14 @@ class ServiceTicket(models.Model): service = models.URLField(verify_exists=False) ticket = models.CharField(max_length=256) created = models.DateTimeField(auto_now=True) - + def __unicode__(self): return "%s (%s) - %s" % (self.user.username, self.service, self.created) - + class LoginTicket(models.Model): ticket = models.CharField(max_length=32) created = models.DateTimeField(auto_now=True) - + def __unicode__(self): return "%s - %s" % (self.ticket, self.created) @@ -30,7 +30,7 @@ def auth_success_response(user): if settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK: callback = get_callable(settings.CAS_CUSTOM_ATTRIBUTES_CALLBACK) attrs = callback(user) - + response = ElementRoot(CAS + 'serviceResponse') auth_success = etree.SubElement(response, CAS + 'authenticationSuccess') username = etree.SubElement(auth_success, CAS + 'user') diff --git a/provider/cas_provider/views.py b/provider/cas_provider/views.py index 2733eb3..e2466b8 100644 --- a/provider/cas_provider/views.py +++ b/provider/cas_provider/views.py @@ -4,6 +4,7 @@ from django.template import RequestContext from django.contrib.auth.models import User from django.contrib.auth import authenticate from django.contrib.auth import login as auth_login, logout as auth_logout +from django.utils.translation import ugettext_lazy as _ from cas_provider.forms import LoginForm from cas_provider.models import ServiceTicket, LoginTicket, auth_success_response @@ -15,14 +16,13 @@ try: from urlparse import parse_qs as url_parse_qs except ImportError: from cgi import parse_qs as url_parse_qs - import logging -logger = logging.getLogger("fnp.cas.provider") +logger = logging.getLogger("cas.provider") __all__ = ['login', 'validate', 'service_validate', 'logout'] -def _add_query_param(url, param, value): +def _add_query_param(url, param, value): parsed = urlparse.urlparse(url) query = url_parse_qs(parsed.query) query[param] = [unicode(value, 'utf-8')] @@ -33,12 +33,10 @@ def _add_query_param(url, param, value): return parsed.geturl() -def login(request, template_name = 'cas/login.html', success_redirect = '/accounts/'): +def login(request, template_name='cas/login.html', success_redirect='/accounts/'): service = request.GET.get('service', None) - + if request.user.is_authenticated(): - logger.info("User %s passed auth, service is %s", request.user, service) - if service is not None: ticket = create_service_ticket(request.user, service) target = _add_query_param(service, 'ticket', ticket.ticket) @@ -47,53 +45,46 @@ def login(request, template_name = 'cas/login.html', success_redirect = '/accoun else: logger.info("Redirecting to default: %s", success_redirect) return HttpResponseRedirect(success_redirect) - + errors = [] if request.method == 'POST': username = request.POST.get('username', None) password = request.POST.get('password', None) service = request.POST.get('service', None) lt = request.POST.get('lt', None) - + logger.debug("User %s logging in", username) - logger.info("Login submit: serivce = %s, Lticket=%s",service, lt) try: - login_ticket = LoginTicket.objects.get(ticket = lt) + login_ticket = LoginTicket.objects.get(ticket=lt) except: - errors.append('Login ticket expired. Please try again.') + errors.append(_(u'Login ticket expired. Please try again.')) else: login_ticket.delete() - logger.debug("Auth") - user = authenticate(username = username, password = password) + user = authenticate(username=username, password=password) if user is not None: if user.is_active: - logger.debug("AuthLogin") auth_login(request, user) - if service is not None: + if service is not None: ticket = create_service_ticket(user, service) - logger.info("Service=%s, ticket=%s", service, ticket) target = _add_query_param(service, 'ticket', ticket.ticket) - logger.info("Redirecting to %s", target) return HttpResponseRedirect(target) else: - logger.info("Redirecting to default: %s", success_redirect) return HttpResponseRedirect(success_redirect) else: - errors.append('This account is disabled.') + errors.append(_(u'This account is disabled.')) else: - errors.append('Incorrect username and/or password.') - - logger.debug("LOGIN GET, service = %s", service) + errors.append(_(u'Incorrect username and/or password.')) + form = LoginForm(service) - return render_to_response(template_name, {'form': form, 'errors': errors}, context_instance = RequestContext(request)) + return render_to_response(template_name, {'form': form, 'errors': errors}, context_instance=RequestContext(request)) def validate(request): service = request.GET.get('service', None) ticket_string = request.GET.get('ticket', None) if service is not None and ticket_string is not None: try: - ticket = ServiceTicket.objects.get(ticket = ticket_string) + ticket = ServiceTicket.objects.get(ticket=ticket_string) username = ticket.user.username ticket.delete() return HttpResponse("yes\n%s\n" % username) @@ -105,24 +96,24 @@ def service_validate(request): service = request.GET.get('service', None) ticket_string = request.GET.get('ticket', None) if service is None or ticket_string is None: - return HttpResponse(''' + return HttpResponse(r''' Not all required parameters were sent. - ''', mimetype = 'text/xml') + ''', mimetype='application/xml') try: - ticket = ServiceTicket.objects.get(ticket = ticket_string) + ticket = ServiceTicket.objects.get(ticket=ticket_string) ticket.delete() - return HttpResponse(auth_success_response(ticket.user), mimetype = 'text/xml') + return HttpResponse(auth_success_response(ticket.user), mimetype='text/xml') except ServiceTicket.DoesNotExist: - return HttpResponse(''' + return HttpResponse(r''' The provided ticket is invalid. - ''', mimetype = 'text/xml') + ''', mimetype='application/xml') -def logout(request, template_name = 'cas/logout.html'): +def logout(request, template_name='cas/logout.html'): url = request.GET.get('url', None) auth_logout(request) - return render_to_response(template_name, {'url': url}, context_instance = RequestContext(request)) + return render_to_response(template_name, {'url': url}, context_instance=RequestContext(request)) diff --git a/provider/setup.py b/provider/setup.py index 489f8d2..8a67235 100644 --- a/provider/setup.py +++ b/provider/setup.py @@ -1,5 +1,5 @@ from setuptools import setup, find_packages - + setup( name='django-cas-provider', version='0.2', diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/forms.py b/src/accounts/forms.py new file mode 100644 index 0000000..5dcb842 --- /dev/null +++ b/src/accounts/forms.py @@ -0,0 +1,26 @@ +from django import forms +from django.contrib.auth.models import User as DjangoUser +from django.utils.translation import ugettext_lazy as _ + +class UserBasicForm(forms.ModelForm): + + class Meta: + model = DjangoUser + fields = ('first_name', 'last_name', 'email',) + + +class UserPasswordForm(forms.Form): + + new_password = forms.CharField(widget=forms.PasswordInput(), + label=_("Your new password")) + verifier = forms.CharField(widget=forms.PasswordInput(), + label=_("Repeated password")) + + def clean(self): + if 'verifier' not in self.cleaned_data or 'new_password' not in self.cleaned_data: + return self.cleaned_data + + if self.cleaned_data['verifier'] != self.cleaned_data['new_password']: + raise forms.ValidationError(_("Passwords do not match!")) + + return self.cleaned_data diff --git a/src/accounts/locale/pl_PL/LC_MESSAGES/django.mo b/src/accounts/locale/pl_PL/LC_MESSAGES/django.mo new file mode 100644 index 0000000..59955ae Binary files /dev/null and b/src/accounts/locale/pl_PL/LC_MESSAGES/django.mo differ diff --git a/src/accounts/locale/pl_PL/LC_MESSAGES/django.po b/src/accounts/locale/pl_PL/LC_MESSAGES/django.po new file mode 100644 index 0000000..8bb2e63 --- /dev/null +++ b/src/accounts/locale/pl_PL/LC_MESSAGES/django.po @@ -0,0 +1,49 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-18 10:48+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:15 +msgid "Your new password" +msgstr "Twoje nowe hasło" + +#: forms.py:17 +msgid "Repeated password" +msgstr "Powtórzone hasło" + +#: forms.py:24 +msgid "Passwords do not match!" +msgstr "Hasło i powtórzenie się nie zgadzają!" + +#: templates/account/profile.html:12 +msgid "Your profile" +msgstr "Twój profil" + +#: templates/account/profile.html:16 +msgid "Change profile" +msgstr "Zmień profil" + +#: templates/account/profile.html:20 +msgid "Password change" +msgstr "Zmiana hasła" + +#: templates/account/profile.html:24 +msgid "Change password" +msgstr "Zmień hasło" + +#: templates/account/profile.html:30 +msgid "Availble services" +msgstr "Dostępne usługi" diff --git a/src/accounts/models.py b/src/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/src/accounts/templates/account/profile.html b/src/accounts/templates/account/profile.html new file mode 100644 index 0000000..15e5eaf --- /dev/null +++ b/src/accounts/templates/account/profile.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% load gravatar i18n %} + +{% block content %} + + +
+ {% if messages %} +

{% for message in messages %}{{ message }}{% endfor %}

+ {% endif %} + +

{% trans "Your profile" %}

+
+ + {{ basic_form.as_table }} + +
+
+ +

{% trans "Password change" %}

+
+ + {{ pass_form.as_table }} + +
+
+
+ +
+

{% trans "Availble services" %}

+ + + + + + +
+{% endblock %} \ No newline at end of file diff --git a/src/accounts/templatetags/__init__.py b/src/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/accounts/templatetags/gravatar.py b/src/accounts/templatetags/gravatar.py new file mode 100644 index 0000000..a6db9aa --- /dev/null +++ b/src/accounts/templatetags/gravatar.py @@ -0,0 +1,44 @@ +from django import template +from django.template.defaultfilters import stringfilter +import hashlib +import urllib + +register = template.Library() + +DEFAULTS = dict(size=80, rating='g', default='monsterid') + +class GravatarNode(template.Node): + + def __init__(self, email, size): + self.email = template.Variable(email) + self.size = size + + def render(self, context): + try: + email = self.email.resolve(context) + except template.VariableDoesNotExist: + return '' + + gravatar_url = "http://www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest() + "?" + gravatar_url += urllib.urlencode({'default': 'wavatar', 'size': str(self.size)}) + + return gravatar_url + +@register.tag +def gravatar(parser, token): + try: + _tag_name, email, size = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires two args" % token.contents.split()[0] + + return GravatarNode(email, int(size)) + + + + +@register.filter(name='md5') +@stringfilter +def md5_hash(value): + h = hashlib.md5() + h.update(value) + return h.hexdigest() diff --git a/src/accounts/tests.py b/src/accounts/tests.py new file mode 100644 index 0000000..2247054 --- /dev/null +++ b/src/accounts/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/src/accounts/urls.py b/src/accounts/urls.py new file mode 100644 index 0000000..86e81fa --- /dev/null +++ b/src/accounts/urls.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.conf.urls.defaults import * + +urlpatterns = patterns('accounts.views', + url(r'^$', 'account_profile'), + url(r'^change_profile$', 'account_change_basic_profile'), + url(r'^change_password$', 'account_change_password'), +) diff --git a/src/accounts/views.py b/src/accounts/views.py new file mode 100644 index 0000000..b00dd20 --- /dev/null +++ b/src/accounts/views.py @@ -0,0 +1,41 @@ +# Create your views here. +from django import http +from django.utils.translation import ugettext as __ +from django.views.generic.simple import direct_to_template +from django.views.decorators.http import require_POST +from django.contrib.auth.decorators import login_required +from accounts.forms import UserBasicForm, UserPasswordForm + +@login_required +def account_profile(request, basic_form=None, pass_form=None): + return direct_to_template(request, "account/profile.html", { + "basic_form": basic_form or UserBasicForm(instance=request.user), + "pass_form": pass_form or UserPasswordForm(), + }) + + +@require_POST +@login_required +def account_change_basic_profile(request): + form = UserBasicForm(request.POST, instance=request.user) + + if form.is_valid(): + form.save() + request.user.message_set.create(message=__("Profile has been changed.")) + return http.HttpResponseRedirect('/accounts/') + + return account_profile(request, basic_form=form) + +@require_POST +@login_required +def account_change_password(request): + form = UserPasswordForm(request.POST) + + if form.is_valid(): + request.user.set_password(form.cleaned_data['new_password']) + request.user.save() + + request.user.message_set.create(message=__("Password has been changed.")) + return http.HttpResponseRedirect('/accounts/') + + return account_profile(request, pass_form=form) diff --git a/src/cas/manage.py b/src/cas/manage.py old mode 100644 new mode 100755 index 741587d..43896be --- a/src/cas/manage.py +++ b/src/cas/manage.py @@ -4,20 +4,20 @@ from django.core.management import execute_manager from os import path import sys -PROJECT_ROOT = path.realpath(path.dirname(__file__)) -sys.path.insert(0, path.abspath(path.join(PROJECT_ROOT, "..", "provider"))) -print sys.path +PROJECT_ROOT = path.realpath(path.dirname(__file__)) +sys.path = [ + path.abspath(path.join(PROJECT_ROOT, '..')), + path.abspath(path.join(PROJECT_ROOT, '..', '..', "provider")), +] + sys.path try: import settings # Assumed to be in the same directory. except ImportError: import traceback - traceback.print_exc(file =sys.stderr) + traceback.print_exc(file=sys.stderr) sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) sys.exit(1) if __name__ == "__main__": # Append lib and apps directories to PYTHONPATH - - execute_manager(settings) diff --git a/src/cas/media/static/css/main.css b/src/cas/media/static/css/main.css new file mode 100644 index 0000000..b2c7b75 --- /dev/null +++ b/src/cas/media/static/css/main.css @@ -0,0 +1,58 @@ +body { + background-color: #eee; +} + +#content_wrap { + background-color: #555; + margin: 0px 5%; + padding: 5px; +} + +#content { + background-color: white; + border-radius: 10px; + margin: 5px; + padding: 0.2em 1em; + overflow: hidden; +} + +#details { + margin: 1em; + float: left; + max-width: 75%; + +} + +#details form table th { + text-align: left; +} + +#details form { + margin-bottom: 2em; +} + +.user_avatar { + float: left; + margin: 1em; + max-width: 20%; + text-align: center; +} + +.user_avatar img { + border: 1px solid black; +} + +#services-list { + width: 250px; + float: right; + margin: 1em 2em; +} + +#services-list img { + width: 250px; + margin: 1em 0em; +} + +img.small_logo { + width: 250px; +} diff --git a/src/cas/media/static/redmine_logo.png b/src/cas/media/static/redmine_logo.png new file mode 100644 index 0000000..bdfabb1 Binary files /dev/null and b/src/cas/media/static/redmine_logo.png differ diff --git a/src/cas/settings.py b/src/cas/settings.py index acc5988..3ed14b0 100644 --- a/src/cas/settings.py +++ b/src/cas/settings.py @@ -6,7 +6,9 @@ PROJECT_ROOT = path.realpath(path.dirname(__file__)) DEBUG = True TEMPLATE_DEBUG = DEBUG -ADMINS = [] +ADMINS = [ + "lrekucki@gmail.com", +] MANAGERS = ADMINS @@ -37,13 +39,11 @@ USE_I18N = True # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = PROJECT_ROOT + '/media/' -STATIC_ROOT = PROJECT_ROOT + '/static/' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '/media/' -STATIC_URL = '/static/' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. @@ -61,6 +61,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.auth", "django.core.context_processors.debug", "django.core.context_processors.i18n", + 'django.core.context_processors.media', "django.core.context_processors.request", ) @@ -75,7 +76,7 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = 'cas.urls' TEMPLATE_DIRS = ( - PROJECT_ROOT + '/templates', + PROJECT_ROOT + '/templates', ) INSTALLED_APPS = ( @@ -85,8 +86,9 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.admin', 'django.contrib.admindocs', - + 'cas_provider', + 'accounts', ) # django-cas-provider settings diff --git a/src/cas/templates/base.html b/src/cas/templates/base.html new file mode 100644 index 0000000..1872c06 --- /dev/null +++ b/src/cas/templates/base.html @@ -0,0 +1,14 @@ + + + + + {% block title %}Fundacja Nowoczesna Polska - CAS{% block subtitle %}{% endblock subtitle %}{% endblock title%} + + {% block extrahead %} + {% endblock %} + + +
{% block content %} {% endblock %}
+ + diff --git a/src/cas/templates/cas/login.html b/src/cas/templates/cas/login.html index e51cd8d..7a51cac 100644 --- a/src/cas/templates/cas/login.html +++ b/src/cas/templates/cas/login.html @@ -1,21 +1,21 @@ -{% extends "cas_base.html" %} +{% extends "base.html" %} +{% load i18n %} {% block content %} +
-
- Zaloguj się - {% if errors %} -
    - {% for error in errors %} -
  • {{ error|escape }}
  • - {% endfor %} -
- {% endif %} - - {{ form.as_table }} -
-

-
+

{% trans "Login" %}

+ + {% for error in errors %} +

{{ error }}

+ {% endfor %} + + + {{ form.as_table }} + +
+
+ + {% include "horizontal_footer.html" %} {% endblock %} - \ No newline at end of file diff --git a/src/cas/templates/cas/logout.html b/src/cas/templates/cas/logout.html index d2a4009..ce3db21 100644 --- a/src/cas/templates/cas/logout.html +++ b/src/cas/templates/cas/logout.html @@ -1,12 +1,21 @@ -{% extends "cas_base.html" %} - +{% extends "base.html" %} +{% load i18n %} + {% block title %} Logged out {% endblock %} - + {% block content %} -

Logged out

- -

You have successfully logged out. To ensure that you are logged out of all services, please close your browser.

- {% if url %}

Click here to return to {{ url }}

{% endif %} +

{% trans "Logged out" %}

+ +

{% blocktrans %}You have successfully logged out. To ensure that you are logged out of all services, please close your browser.{% endblocktrans %}

+ + {% if url %} +

{% blocktrans %}You can return to service you came from: {{ url }}{% endblocktrans %}

+ {% endif %} + + {% url cas_provider.views.login as login_url %} +

{% blocktrans %}You can also login again{% endblocktrans %}

+ + {% include "horizontal_footer.html" %} {% endblock %} \ No newline at end of file diff --git a/src/cas/templates/cas_base.html b/src/cas/templates/cas_base.html deleted file mode 100644 index a49c721..0000000 --- a/src/cas/templates/cas_base.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - {% block title %}Fundacja Nowoczesna Polska - CAS{% block subtitle %}{% endblock subtitle %}{% endblock title%} - {% block extrahead %} - {% endblock %} - - -
{% block content %} {% endblock %}
- - diff --git a/src/cas/templates/horizontal_footer.html b/src/cas/templates/horizontal_footer.html new file mode 100644 index 0000000..4d6ab86 --- /dev/null +++ b/src/cas/templates/horizontal_footer.html @@ -0,0 +1,9 @@ +
+

+ + + + + + +

\ No newline at end of file diff --git a/src/cas/urls.py b/src/cas/urls.py index f1aeca6..4a285fd 100644 --- a/src/cas/urls.py +++ b/src/cas/urls.py @@ -1,17 +1,25 @@ # -*- coding: utf-8 -*- from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to from django.contrib import admin from django.conf import settings admin.autodiscover() urlpatterns = patterns('', + url(r'^$', redirect_to, {'url': '/accounts/'}), + + # django-cas-provider + url(r'^cas/', include('cas_provider.urls')), + # Admin panel url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/(.*)', admin.site.root), + url(r'^admin/', include(admin.site.urls)), - # django-cas-provider - url(r'^', include('cas_provider.urls')), + url(r'^accounts/', include('accounts.urls')), + + url(r'^%s(?P.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve', + {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), )