Skip to content
Browse files

new skel: signup

  • Loading branch information...
1 parent 5e112ea commit 7e5bf91967863400e546f335e5874f3f7e41236b @fiorix committed
Showing with 2,519 additions and 9 deletions.
  1. +1 −0 MANIFEST.in
  2. +1 −1 appskel/default/modname/views.py
  3. +0 −3 appskel/default/modname/web.py
  4. +3 −0 appskel/signup/.gitignore
  5. +115 −0 appskel/signup/README.md
  6. BIN appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.mo
  7. +195 −0 appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.po
  8. BIN appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.mo
  9. +195 −0 appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.po
  10. BIN appskel/signup/frontend/static/favicon.ico
  11. +1 −0 appskel/signup/frontend/static/legal.txt
  12. +64 −0 appskel/signup/frontend/template/account.html
  13. +44 −0 appskel/signup/frontend/template/admin.html
  14. +27 −0 appskel/signup/frontend/template/base.html
  15. +13 −0 appskel/signup/frontend/template/dashboard.html
  16. +43 −0 appskel/signup/frontend/template/error_all.html
  17. +107 −0 appskel/signup/frontend/template/index.html
  18. +78 −0 appskel/signup/frontend/template/passwd.html
  19. +17 −0 appskel/signup/frontend/template/passwd_email.html
  20. +1 −0 appskel/signup/frontend/template/passwd_email_subject.txt
  21. +57 −0 appskel/signup/frontend/template/passwd_ok.html
  22. +76 −0 appskel/signup/frontend/template/signin.html
  23. +84 −0 appskel/signup/frontend/template/signup.html
  24. +18 −0 appskel/signup/frontend/template/signup_email.html
  25. +1 −0 appskel/signup/frontend/template/signup_email_subject.txt
  26. +57 −0 appskel/signup/frontend/template/signup_ok.html
  27. +40 −0 appskel/signup/modname.conf
  28. +13 −0 appskel/signup/modname.sql
  29. +6 −0 appskel/signup/modname/__init__.py
  30. +92 −0 appskel/signup/modname/config.py
  31. +94 −0 appskel/signup/modname/storage.py
  32. +325 −0 appskel/signup/modname/txdbapi.py
  33. +125 −0 appskel/signup/modname/utils.py
  34. +331 −0 appskel/signup/modname/views.py
  35. +39 −0 appskel/signup/modname/web.py
  36. +10 −0 appskel/signup/scripts/cookie_secret.py
  37. +89 −0 appskel/signup/scripts/debian-init.d
  38. +112 −0 appskel/signup/scripts/debian-multicore-init.d
  39. +21 −0 appskel/signup/scripts/localefix.py
  40. +6 −0 appskel/signup/start.sh
  41. +1 −1 cyclone/__init__.py
  42. +3 −1 cyclone/app.py
  43. BIN cyclone/appskel_default.zip
  44. BIN cyclone/appskel_foreman.zip
  45. BIN cyclone/appskel_signup.zip
  46. +9 −0 debian/changelog
  47. +5 −3 setup.py
View
1 MANIFEST.in
@@ -1,2 +1,3 @@
include cyclone/appskel_default.zip
include cyclone/appskel_foreman.zip
+include cyclone/appskel_signup.zip
View
2 appskel/default/modname/views.py
@@ -69,6 +69,6 @@ def get(self):
log.msg("MySQL query failed: %s" % str(e))
raise cyclone.web.HTTPError(503) # Service Unavailable
else:
- self.write({"response": response})
+ self.write({"response": str(response[0][0])})
else:
self.write("MySQL is disabled\r\n")
View
3 appskel/default/modname/web.py
@@ -5,9 +5,6 @@
import cyclone.locale
import cyclone.web
-import cyclone.locale
-import cyclone.web
-
from $modname import views
from $modname import config
from $modname.storage import DatabaseMixin
View
3 appskel/signup/.gitignore
@@ -0,0 +1,3 @@
+*.swp
+*.pyc
+dropin.cache
View
115 appskel/signup/README.md
@@ -0,0 +1,115 @@
+# cyclone-based project
+
+ This is the source code of $project_name
+ $name <$email>
+
+## About
+
+This file has been created automatically by cyclone-tool for $project_name.
+It contains the following files:
+
+- ``start.sh``: simple shell script to start the server
+- ``$modname.conf``: configuration file for the web server
+- ``$modname/__init__.py``: information such as author and version of this package
+- ``$modname/web.py``: map of url handlers and main class of the web server
+- ``$modname/config.py``: configuration parser for ``$modname.conf``
+- ``$modname/views.py``: code of url handlers for the web server
+- ``scripts/debian-init.d``: generic debian start/stop init script
+- ``scripts/debian-multicore-init.d``: run one instance per core on debian
+- ``scripts/localefix.py``: script to fix html text before running ``xgettext``
+- ``scripts/cookie_secret.py``: script for generating new secret key for the web server
+
+### Running
+
+For development and testing:
+
+ twistd -n cyclone --help
+ twistd -n cyclone -r $modname.web.Application [--help]
+
+For production:
+
+ twistd cyclone \
+ --logfile=/var/log/$project.log \
+ --pidfile=/var/run/$project.pid \
+ -r $modname.web.Application
+
+
+### Convert this document to HTML
+
+Well, since this is a web server, it might be a good idea to convert this document
+to HTML before getting into customization details.
+
+This can be done using [markdown](http://daringfireball.net/projects/markdown/).
+
+ brew install markdown
+ markdown README.md > frontend/static/readme.html
+
+And point your browser to <http://localhost:8888/static/readme.html> after this server
+is running.
+
+## Customization
+
+This section is dedicated to explaining how to customize your brand new package.
+
+### Databases
+
+cyclone provides built-in support for SQLite and Redis databases.
+It also supports any RDBM supported by the ``twisted.enterprise.adbapi`` module,
+like MySQL or PostgreSQL.
+
+The default configuration file ``$modname.conf`` ships with pre-configured
+settings for SQLite, Redis and MySQL.
+
+The code for loading all the database settings is in ``$modname/config.py``.
+Feel free to comment or even remove such code, and configuration entries. It
+shouldn't break the web server.
+
+Take a look at ``$modname/utils.py``, which is where persistent database
+connections are initialized.
+
+
+### Internationalization
+
+cyclone uses the standard ``gettext`` library for dealing with string
+translation.
+
+Make sure you have the ``gettext`` package installed. If you don't, you won't
+be able to translate your software.
+
+For installing the ``gettext`` package on Debian and Ubuntu systems, do this:
+
+ apt-get install gettext
+
+For Mac OS X, I'd suggest using [HomeBrew](http://mxcl.github.com/homebrew>).
+If you already use HomeBrew, run:
+
+ brew install gettext
+ brew link gettext
+
+For generating translatable files for HTML and Python code of your software,
+run this:
+
+ cat frontend/template/*.html $modname/*.py | python scripts/localefix.py | \
+ xgettext - --language=Python --from-code=utf-8 --keyword=_:1,2 -d $modname
+
+Then translate $modname.po, compile and copy to the appropriate locale
+directory:
+
+ (pt_BR is used as example here)
+ vi $modname.po
+ mkdir -p frontend/locale/pt_BR/LC_MESSAGES/
+ msgfmt $modname.po -o frontend/locale/pt_BR/LC_MESSAGES/$modname.mo
+
+There are sample translations for both Spanish and Portuguese in this package,
+already compiled.
+
+
+### Cookie Secret
+
+The current cookie secret key in ``$modname.conf`` was generated during the
+creation of this package. However, if you need a new one, you may run the
+``scripts/cookie_secret.py`` script to generate a random key.
+
+## Credits
+
+- [cyclone](http://github.com/fiorix/cyclone) web server.
View
BIN appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.mo
Binary file not shown.
View
195 appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.po
@@ -0,0 +1,195 @@
+# cyclone signup template.
+# Copyright (C) 2012 cyclone
+# This file is distributed under the same license as the cyclone package.
+# Alexandre Fiori <fiorix@gmail.com>, 2012.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-12-11 23:20-0500\n"
+"PO-Revision-Date: 2012-12-12 00:00:00+0500\n"
+"Last-Translator: Alexandre Fiori <fiorix@gmail.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: standard input:36
+msgid "Account settings"
+msgstr "Datos de la conta"
+
+#: standard input:37
+msgid "Name"
+msgstr "Nombre"
+
+#: standard input:39
+msgid "Password"
+msgstr "Contraseña"
+
+#: standard input:45
+msgid "Invalid name"
+msgstr "Nombra inválido"
+
+#: standard input:47
+msgid "Invalid password"
+msgstr "Contraseña inválida"
+
+#: standard input:49
+msgid "Passwords don't match"
+msgstr "Las contraseñas no coinciden"
+
+#: standard input:53
+msgid "Saved!"
+msgstr "Guardado!"
+
+#: standard input:55
+msgid "Update"
+msgstr "Actualizar"
+
+#: standard input:84
+msgid "Dashboard"
+msgstr "Tablero"
+
+#: standard input:90
+msgid "Account"
+msgstr "Cuenta"
+
+#: standard input:92
+msgid "Language"
+msgstr "Idioma"
+
+#: standard input:100
+msgid "Sign out"
+msgstr "Cerrar"
+
+#: standard input:139
+msgid "Bootstrap starter template"
+msgstr "Template inicial de Bootstrap"
+
+#: standard input:140
+msgid ""
+"Use this document as a way to quick start any new project.<br> All you get "
+"is this message and a barebones HTML document."
+msgstr ""
+"Utilice este documento para cualquier nuevo proyecto como una forma de "
+"de inicio rápido.<br> Solo tiene es este mensaje y un documento básico HTML."
+
+#: standard input:246 standard input:352 standard input:430 standard input:504
+#: standard input:580 standard input:665
+msgid "Home"
+msgstr "Inicio"
+
+#: standard input:247 standard input:353 standard input:431 standard input:505
+#: standard input:513 standard input:522 standard input:581 standard input:666
+msgid "Sign in"
+msgstr "Entrar"
+
+#: standard input:255
+msgid "Super awesome marketing speak!"
+msgstr "Hablar de marketing impressionante!"
+
+#: standard input:257
+msgid "Sign up today"
+msgstr "Inscríbase hoy"
+
+#: standard input:361
+msgid "Reset password"
+msgstr "Restablecer contraseña"
+
+#: standard input:366 standard input:594
+msgid "Invalid email address"
+msgstr "Correo electrónico no válido"
+
+#: standard input:368
+msgid "Email address not registered"
+msgstr "Correo electrónico no registrada"
+
+#: standard input:370 standard input:598
+msgid "Please try again later"
+msgstr "Por favor inténtelo más tarde"
+
+#: standard input:374
+msgid "Send"
+msgstr "Enviar"
+
+#: standard input:386
+msgid "New password"
+msgstr "Nueva contraseña"
+
+#: standard input:387
+msgid ""
+"Your password has been reset, and must be used to reactivate the account "
+"within 24 hours."
+msgstr ""
+"La contraseña se ha restablecido y debe utilizarse para reactivar la cuenta "
+"dentro de 24 horas."
+
+#: standard input:388
+msgid "Here's the new password:"
+msgstr "Aquí está la nueva contraseña:"
+
+#: standard input:391 standard input:625
+#, python-format
+msgid "Requested by IP %s on %s"
+msgstr "Solicitado por IP %s en %s"
+
+#: standard input:440 standard input:675
+#, python-format
+msgid "A confirmation email has been sent to <b>%s</b> with instructions."
+msgstr "Ha enviado un email de confirmación a <b>%s</b> con instrucciones."
+
+#: standard input:517
+msgid "Invalid email or password"
+msgstr "Correo electrónico o contraseña no válida"
+
+#: standard input:520
+msgid "Remember me"
+msgstr "Recuerde"
+
+#: standard input:523
+msgid "Forgot password?"
+msgstr "¿Olvidó la contraseña?"
+
+#: standard input:589 standard input:608
+msgid "Sign up"
+msgstr "Registrarse"
+
+#: standard input:596
+msgid "Email already registered"
+msgstr "Correo electrónico registrado"
+
+#: standard input:603
+msgid "I agree with the "
+msgstr "Estoy de acuerdo con los "
+
+#: standard input:603
+msgid "Terms of Service"
+msgstr "Términos de Servicio"
+
+#: standard input:606
+msgid "Please accept the Terms of Service"
+msgstr "Por favor, acepte los Términos de Servicio"
+
+#: standard input:620
+msgid "Welcome!"
+msgstr "¡Bienvenido!"
+
+#: standard input:621
+msgid "Your account has been created, and must be activated within 24 hours."
+msgstr "Su cuenta ha sido creada y debe activarse dentro de 24 horas."
+
+#: standard input:622
+msgid "Here's your password:"
+msgstr "Aquí está la contraseña:"
+
+#: misc
+msgid "Full name"
+msgstr "Nombre completo"
+
+msgid "Email address"
+msgstr "Correo electrónico"
+
+msgid "Confirm"
+msgstr "Confirmación"
View
BIN appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.mo
Binary file not shown.
View
195 appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.po
@@ -0,0 +1,195 @@
+# cyclone signup template.
+# Copyright (C) 2012 cyclone
+# This file is distributed under the same license as the cyclone package.
+# Alexandre Fiori <fiorix@gmail.com>, 2012.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-12-11 23:20-0500\n"
+"PO-Revision-Date: 2012-12-12 00:00:00+0500\n"
+"Last-Translator: Alexandre Fiori <fiorix@gmail.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: standard input:36
+msgid "Account settings"
+msgstr "Dados da conta"
+
+#: standard input:37
+msgid "Name"
+msgstr "Nome"
+
+#: standard input:39
+msgid "Password"
+msgstr "Senha"
+
+#: standard input:45
+msgid "Invalid name"
+msgstr "Nome inválido"
+
+#: standard input:47
+msgid "Invalid password"
+msgstr "Senha inválida"
+
+#: standard input:49
+msgid "Passwords don't match"
+msgstr "As senhas não coincidem"
+
+#: standard input:53
+msgid "Saved!"
+msgstr "Gravado!"
+
+#: standard input:55
+msgid "Update"
+msgstr "Atualizar"
+
+#: standard input:84
+msgid "Dashboard"
+msgstr "Painel"
+
+#: standard input:90
+msgid "Account"
+msgstr "Conta"
+
+#: standard input:92
+msgid "Language"
+msgstr "Idioma"
+
+#: standard input:100
+msgid "Sign out"
+msgstr "Sair"
+
+#: standard input:139
+msgid "Bootstrap starter template"
+msgstr "Template inicial do Bootstrap"
+
+#: standard input:140
+msgid ""
+"Use this document as a way to quick start any new project.<br> All you get "
+"is this message and a barebones HTML document."
+msgstr ""
+"Use este documento para iniciar qualquer novo projeto rapidamente.<br> Tudo "
+"que há nele é esta mensagem e um documento HTML puro."
+
+#: standard input:246 standard input:352 standard input:430 standard input:504
+#: standard input:580 standard input:665
+msgid "Home"
+msgstr "Início"
+
+#: standard input:247 standard input:353 standard input:431 standard input:505
+#: standard input:513 standard input:522 standard input:581 standard input:666
+msgid "Sign in"
+msgstr "Entrar"
+
+#: standard input:255
+msgid "Super awesome marketing speak!"
+msgstr "Linguagem de marketing impressionante!"
+
+#: standard input:257
+msgid "Sign up today"
+msgstr "Cadastre-se hoje"
+
+#: standard input:361
+msgid "Reset password"
+msgstr "Reiniciar a senha"
+
+#: standard input:366 standard input:594
+msgid "Invalid email address"
+msgstr "Endereço de email inválido"
+
+#: standard input:368
+msgid "Email address not registered"
+msgstr "Endereço de email não registrado"
+
+#: standard input:370 standard input:598
+msgid "Please try again later"
+msgstr "Por favor tente novamente mais tarde"
+
+#: standard input:374
+msgid "Send"
+msgstr "Enviar"
+
+#: standard input:386
+msgid "New password"
+msgstr "Nova senha"
+
+#: standard input:387
+msgid ""
+"Your password has been reset, and must be used to reactivate the account "
+"within 24 hours."
+msgstr ""
+"Sua senha foi reiniciada, e deve ser usada para reativar a conta em até "
+"24 horas."
+
+#: standard input:388
+msgid "Here's the new password:"
+msgstr "Aqui está sua nova senha:"
+
+#: standard input:391 standard input:625
+#, python-format
+msgid "Requested by IP %s on %s"
+msgstr "Solicitado por IP %s em %s"
+
+#: standard input:440 standard input:675
+#, python-format
+msgid "A confirmation email has been sent to <b>%s</b> with instructions."
+msgstr "Um email de confirmação foi enviado para <b>%s</b> com instruções."
+
+#: standard input:517
+msgid "Invalid email or password"
+msgstr "Endereço de email ou senha inválidos"
+
+#: standard input:520
+msgid "Remember me"
+msgstr "Lembre-me"
+
+#: standard input:523
+msgid "Forgot password?"
+msgstr "Esqueceu a senha?"
+
+#: standard input:589 standard input:608
+msgid "Sign up"
+msgstr "Cadastre-se"
+
+#: standard input:596
+msgid "Email already registered"
+msgstr "Email já registrado"
+
+#: standard input:603
+msgid "I agree with the "
+msgstr "Eu concordo com os "
+
+#: standard input:603
+msgid "Terms of Service"
+msgstr "Termos de Serviço"
+
+#: standard input:606
+msgid "Please accept the Terms of Service"
+msgstr "Por favor aceite os Termos de Serviço"
+
+#: standard input:620
+msgid "Welcome!"
+msgstr "Bem vindo!"
+
+#: standard input:621
+msgid "Your account has been created, and must be activated within 24 hours."
+msgstr "Sua conta foi criada, e deve ser ativada em até 24 horas."
+
+#: standard input:622
+msgid "Here's your password:"
+msgstr "Aqui está sua senha:"
+
+#: misc
+msgid "Full name"
+msgstr "Nome completo"
+
+msgid "Email address"
+msgstr "Endereço de email"
+
+msgid "Confirm"
+msgstr "Confirmação"
View
BIN appskel/signup/frontend/static/favicon.ico
Binary file not shown.
View
1 appskel/signup/frontend/static/legal.txt
@@ -0,0 +1 @@
+Terms of Service: whatever.
View
64 appskel/signup/frontend/template/account.html
@@ -0,0 +1,64 @@
+{% extends "admin.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 60px;
+ }
+ .form-account {
+ max-width: 300px;
+ padding: 19px 29px 29px;
+ margin: 0 auto 20px;
+ background-color: #fff;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ }
+ .form-account .form-account-heading,
+ .form-account .checkbox {
+ margin-bottom: 10px;
+ }
+ .form-account input[type="text"],
+ .form-account input[type="password"] {
+ font-size: 16px;
+ height: auto;
+ margin-bottom: 15px;
+ padding: 7px 9px;
+ }
+</style>
+{% end %}
+
+{% block page %}
+<form class="form-account" method="post">
+ <h2 class="form-account-heading">{{_("Account settings")}}</h2>
+ <label>{{_("Name")}}</label>
+ <input type="text" class="input-block-level" placeholder="{{_('Full name')}}" name="full_name" id="full_name" value="{% if fields.full_name %}{{fields.full_name}}{% end %}">
+ <label>{{_("Password")}}</label>
+ <input type="password" class="input-block-level" placeholder="{{_('New password')}}" name="passwd_1">
+ <input type="password" class="input-block-level" placeholder="{{_('Confirm')}}" name="passwd_2">
+ {% if fields.err %}
+ <div class="alert alert-error">
+ {% if "invalid_name" in fields.err %}
+ {{_("Invalid name")}}
+ {% elif "invalid_passwd" in fields.err %}
+ {{_("Invalid password")}}
+ {% elif "nomatch" in fields.err %}
+ {{_("Passwords don't match")}}
+ {% end %}
+ <a class="close" data-dismiss="alert">×</a></div>
+ {% elif fields.updated %}
+ <div class="alert alert-success">{{_("Saved!")}}<a class="close" data-dismiss="alert">×</a></div>
+ {% end %}
+ <button class="btn btn-large btn-primary" type="submit">{{_("Update")}}</button>
+</form>
+
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
44 appskel/signup/frontend/template/admin.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {padding-top: 60px;}
+</style>
+{% end %}
+
+{% block navbar %}
+<div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" href="/">$project</a>
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ <li class="{% block dashboard_menu %}{% end %}"><a href="/dashboard">{{_("Dashboard")}}</a></li>
+ </ul>
+ <ul class="nav pull-right">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{handler.current_user["email"]}}<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="account">{{_("Account")}}</a></li>
+ <li class="dropdown-submenu">
+ <a tabindex="-1" href="#">{{_("Language")}}</a>
+ <ul class="dropdown-menu">
+ <li {% if handler.locale.code == "en_US" %}class="active"{% end %}><a href="/lang/en_US">English</i></a></li>
+ <li {% if handler.locale.code == "es_ES" %}class="active"{% end %}><a href="/lang/es_ES">Español</a></li>
+ <li {% if handler.locale.code == "pt_BR" %}class="active"{% end %}><a href="/lang/pt_BR">Português</a></li>
+ </ul>
+ </li>
+ <li class="divider"></li>
+ <li><a href="signout">{{_("Sign out")}}</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div><!--/.nav-collapse -->
+ </div>
+ </div>
+</div>
+{% end %}
View
27 appskel/signup/frontend/template/base.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>{% block title %}$project{% end %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <link rel="shortcut icon" type="image/ico" href="{{static_url('favicon.ico')}}">
+ <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap-combined.min.css" rel="stylesheet">
+ {% block header %}{% end %}
+ </head>
+
+ <body>
+ {% block navbar %}{% end %}
+
+ <div class="container">
+ {% block page %}{% end %}
+ </div> <!-- /container -->
+
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
+ <script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/js/bootstrap.min.js"></script>
+
+ {% block scripts %}{% end %}
+ </body>
+</html>
View
13 appskel/signup/frontend/template/dashboard.html
@@ -0,0 +1,13 @@
+{% extends "admin.html" %}
+{% block dashboard_menu %}active{% end %}
+{% block page %}
+<h1>{{_("Bootstrap starter template")}}</h1>
+<p>{{_("Use this document as a way to quick start any new project.<br> All you get is this message and a barebones HTML document.")}}</p>
+
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
43 appskel/signup/frontend/template/error_all.html
@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ }
+
+ .container {
+ margin: 0 auto;
+ max-width: 900px;
+ }
+ .container > hr {
+ margin: 30px 0;
+ }
+
+ .jumbotron {
+ margin: 60px 0;
+ text-align: left;
+ }
+ .jumbotron h1 {
+ font-size: 72px;
+ line-height: 1;
+ }
+</style>
+{% end %}
+
+{% block page %}
+<div class="jumbotron">
+ <h1>Oooops!</h1>
+ <p class="lead">{{"HTTP %s: %s" % (fields["code"], fields["message"])}}</p>
+
+ {% if "exception" in fields and handler.settings.debug is True %}
+ <p>Debug:</p>
+ <pre>{{fields["exception"]}}</pre>
+ {% end %}
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
107 appskel/signup/frontend/template/index.html
@@ -0,0 +1,107 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ }
+
+ /* Custom container */
+ .container {
+ margin: 0 auto;
+ max-width: 700px;
+ }
+ .container > hr {
+ margin: 30px 0;
+ }
+
+ /* Main marketing message and sign up button */
+ .jumbotron {
+ margin: 60px 0;
+ text-align: center;
+ }
+ .jumbotron h1 {
+ font-size: 72px;
+ line-height: 1;
+ }
+ .jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px;
+ }
+
+ /* Supporting marketing content */
+ .marketing {
+ margin: 60px 0;
+ }
+ .marketing p + h4 {
+ margin-top: 28px;
+ }
+
+ #navlist {
+ margin-top: 60px;
+ }
+
+ #navlist li {
+ display: inline;
+ list-style-type: none;
+ padding-right: 10px;
+ }
+</style>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li class="active"><a href="/">{{_("Home")}}</a></li>
+ <li><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<div class="jumbotron">
+ <h1>{{_("Super awesome marketing speak!")}}</h1>
+ <p class="lead">Cras justo odio, dapibus ac facilisis in, egestas eget quam. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
+ <a class="btn btn-large btn-success" href="signup">{{_("Sign up today")}}</a>
+
+ <ul id="navlist">
+ <li><a href="/lang/en_US">English</a></li>
+ <li><a href="/lang/es_ES">Español</a></li>
+ <li><a href="/lang/pt_BR">Português</a></li>
+ </ul>
+</div>
+
+<hr>
+
+<div class="row-fluid marketing">
+ <div class="span6">
+ <h4>Subheading</h4>
+ <p>Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.</p>
+
+ <h4>Subheading</h4>
+ <p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.</p>
+
+ <h4>Subheading</h4>
+ <p>Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+ </div>
+
+ <div class="span6">
+ <h4>Subheading</h4>
+ <p>Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.</p>
+
+ <h4>Subheading</h4>
+ <p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.</p>
+
+ <h4>Subheading</h4>
+ <p>Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+ </div>
+</div>
+
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
78 appskel/signup/frontend/template/passwd.html
@@ -0,0 +1,78 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ background-color: #f5f5f5;
+ }
+
+ .form-passwd {
+ max-width: 300px;
+ padding: 19px 29px 29px;
+ margin: 0 auto 20px;
+ background-color: #fff;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ }
+ .form-passwd .form-passwd-heading,
+ .form-passwd .checkbox {
+ margin-bottom: 10px;
+ }
+ .form-passwd input[type="text"],
+ .form-passwd input[type="password"] {
+ font-size: 16px;
+ height: auto;
+ margin-bottom: 15px;
+ padding: 7px 9px;
+ }
+</style>
+{% end %}
+
+{% block scripts %}
+<script>
+$$(document).ready(function(){
+ $$("#email").focus();
+ $$("#email").keypress(function (e){
+ if (e.which == 13) {
+ $$("form").submit();
+ return false;
+ }
+ });
+});
+</script>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="/">{{_("Home")}}</a></li>
+ <li><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<form class="form-passwd" method="post">
+ <h2 class="form-passwd-heading">{{_("Reset password")}}</h2>
+ <input type="text" class="input-block-level" placeholder="{{_('Email address')}}" name="email" id="email" value="{% if fields.email %}{{fields.email}}{% end %}">
+ {% if fields.err %}
+ <div class="alert alert-error">
+ {% if "email" in fields.err %}
+ {{_("Invalid email address")}}
+ {% elif "notfound" in fields.err %}
+ {{_("Email address not registered")}}
+ {% elif "send" in fields.err %}
+ {{_("Please try again later")}}
+ {% end %}
+ <a class="close" data-dismiss="alert">×</a></div>
+ {% end %}
+ <button class="btn btn-large btn-primary" type="submit">{{_("Send")}}</button>
+</form>
+{% end %}
View
17 appskel/signup/frontend/template/passwd_email.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ </head>
+
+ <body style="padding:20px;background-color:rgb(255,255,255);font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;">
+ <h1 style="margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility;font-size:36px;line-height:40px;color:rgb(51,51,51);">{{_("New password")}}</h1>
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Your password has been reset, and must be used to reactivate the account within 24 hours.")}}</p>
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Here's the new password:")}} <span style="font-weight:bold;color:rgb(70,136,71);">{{passwd}}</span></p>
+
+ <hr style="border-top:1px dotted rgb(51,51,51);">
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Requested by IP %s on %s") % (ip, date)}}</p>
+ </body>
+</html>
View
1 appskel/signup/frontend/template/passwd_email_subject.txt
@@ -0,0 +1 @@
+$project: {{_("Password reset")}}
View
57 appskel/signup/frontend/template/passwd_ok.html
@@ -0,0 +1,57 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ }
+
+ /* Custom container */
+ .container {
+ margin: 0 auto;
+ max-width: 700px;
+ }
+ .container > hr {
+ margin: 30px 0;
+ }
+
+ /* Main marketing message and sign up button */
+ .jumbotron {
+ margin: 60px 0;
+ text-align: center;
+ }
+ .jumbotron h1 {
+ font-size: 72px;
+ line-height: 1;
+ }
+ .jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px;
+ }
+</style>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="/">{{_("Home")}}</a></li>
+ <li><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<div class="jumbotron">
+ <h1>Done!</h1>
+ <p class="lead">{{_("A confirmation email has been sent to <b>%s</b> with instructions.") % email}}</p>
+ <a class="btn btn-large btn-success" href="signin">Sign in</a>
+</div>
+
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
76 appskel/signup/frontend/template/signin.html
@@ -0,0 +1,76 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ background-color: #f5f5f5;
+ }
+
+ .form-signin {
+ max-width: 300px;
+ padding: 19px 29px 29px;
+ margin: 0 auto 20px;
+ background-color: #fff;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ }
+ .form-signin .form-signin-heading,
+ .form-signin .checkbox {
+ margin-bottom: 10px;
+ }
+ .form-signin input[type="text"],
+ .form-signin input[type="password"] {
+ font-size: 16px;
+ height: auto;
+ margin-bottom: 15px;
+ padding: 7px 9px;
+ }
+</style>
+{% end %}
+
+{% block scripts %}
+<script>
+$$(document).ready(function(){
+ $$("#email").focus();
+ $$("#email").keypress(function (e){
+ if (e.which == 13) {
+ $$("form").submit();
+ return false;
+ }
+ });
+});
+</script>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="/">{{_("Home")}}</a></li>
+ <li class="active"><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<form class="form-signin" method="post">
+ <h2 class="form-signin-heading">{{_("Sign in")}}</h2>
+ <input type="text" class="input-block-level" placeholder="{{_('Email address')}}" name="email" id="email" value="{% if fields.email %}{{fields.email}}{% end %}">
+ <input type="password" class="input-block-level" placeholder="{{_('Password')}}" name="passwd">
+ {% if fields.err and "auth" in fields.err %}
+ <div class="alert alert-error">{{_("Invalid email or password")}}<a class="close" data-dismiss="alert">×</a></div>
+ {% end %}
+ <label class="checkbox">
+ <input type="checkbox" name="remember" {% if fields.remember %}checked{% end %}> {{_("Remember me")}}
+ </label>
+ <button class="btn btn-large btn-primary" type="submit">{{_("Sign in")}}</button>
+ <a style="margin-left:10px" href="passwd">{{_("Forgot password?")}}</a>
+</form>
+{% end %}
+
View
84 appskel/signup/frontend/template/signup.html
@@ -0,0 +1,84 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ background-color: #f5f5f5;
+ }
+
+ .form-signup {
+ max-width: 300px;
+ padding: 19px 29px 29px;
+ margin: 0 auto 20px;
+ background-color: #fff;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ box-shadow: 0 1px 2px rgba(0,0,0,.05);
+ }
+ .form-signup .form-signup-heading,
+ .form-signup .checkbox {
+ margin-bottom: 10px;
+ }
+ .form-signup input[type="text"],
+ .form-signup input[type="password"] {
+ font-size: 16px;
+ height: auto;
+ margin-bottom: 15px;
+ padding: 7px 9px;
+ }
+</style>
+{% end %}
+
+{% block scripts %}
+<script>
+$$(document).ready(function(){
+ $$("#email").focus();
+ $$("#email").keypress(function (e){
+ if (e.which == 13) {
+ $$("form").submit();
+ return false;
+ }
+ });
+});
+</script>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="/">{{_("Home")}}</a></li>
+ <li><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<form class="form-signup" method="post">
+ <h2 class="form-signup-heading">{{_("Sign up")}}</h2>
+ <input type="text" class="input-block-level" placeholder="{{_('Email address')}}" name="email" id="email" value="{% if fields.email %}{{fields.email}}{% end %}">
+ {% if fields.err and "legal" not in fields.err %}
+ <div class="alert alert-error">
+ {% if "email" in fields.err %}
+ {{_("Invalid email address")}}
+ {% elif "exists" in fields.err %}
+ {{_("Email already registered")}}
+ {% elif "send" in fields.err %}
+ {{_("Please try again later")}}
+ {% end %}
+ <a class="close" data-dismiss="alert">×</a></div>
+ {% end %}
+ <label class="checkbox">
+ <input type="checkbox" name="legal" {% if fields.legal == "on" %}checked{% end %}> {{_("I agree with the ")}}<a href="/legal">{{_("Terms of Service")}}</a>
+ </label>
+ {% if fields.err and "legal" in fields.err %}
+ <div class="alert alert-error">{{_("Please accept the Terms of Service")}}<a class="close" data-dismiss="alert">×</a></div>
+ {% end %}
+ <button class="btn btn-large btn-success" type="submit">{{_("Sign up")}}</button>
+</form>
+{% end %}
View
18 appskel/signup/frontend/template/signup_email.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ </head>
+
+ <body style="padding:20px;background-color:rgb(255,255,255);font-family:Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;">
+ <h1 style="margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility;font-size:36px;line-height:40px;color:rgb(51,51,51);">{{_("Welcome!")}}</h1>
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Your account has been created, and must be activated within 24 hours.")}}</p>
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Here's your password:")}} <span style="font-weight:bold;color:rgb(70,136,71);">{{passwd}}</span></p>
+
+ <hr style="border-top:1px dotted rgb(51,51,51);">
+ <p style="font-family:inherit;color:rgb(51,51,51);">{{_("Requested by IP %s on %s") % (ip, date)}}</p>
+ </body>
+</html>
+
View
1 appskel/signup/frontend/template/signup_email_subject.txt
@@ -0,0 +1 @@
+$project: {{_("Sign up confirmation")}}
View
57 appskel/signup/frontend/template/signup_ok.html
@@ -0,0 +1,57 @@
+{% extends "base.html" %}
+{% block header %}
+<style>
+ body {
+ padding-top: 20px;
+ padding-bottom: 40px;
+ }
+
+ /* Custom container */
+ .container {
+ margin: 0 auto;
+ max-width: 700px;
+ }
+ .container > hr {
+ margin: 30px 0;
+ }
+
+ /* Main marketing message and sign up button */
+ .jumbotron {
+ margin: 60px 0;
+ text-align: center;
+ }
+ .jumbotron h1 {
+ font-size: 72px;
+ line-height: 1;
+ }
+ .jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px;
+ }
+</style>
+{% end %}
+
+{% block page %}
+<div class="masthead">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="/">{{_("Home")}}</a></li>
+ <li><a href="/signin">{{_("Sign in")}}</a></li>
+ </ul>
+ <h3 class="muted">$project</h3>
+</div>
+
+<hr>
+
+<div class="jumbotron">
+ <h1>Thank you!</h1>
+ <p class="lead">{{_("A confirmation email has been sent to <b>%s</b> with instructions.") % email}}</p>
+ <a class="btn btn-large btn-success" href="signin">Sign in</a>
+</div>
+
+<hr>
+
+<div class="footer">
+ <p>&copy; Company 2012</p>
+</div>
+
+{% end %}
View
40 appskel/signup/modname.conf
@@ -0,0 +1,40 @@
+[server]
+debug = true
+xheaders = false
+xsrf_cookies = false
+cookie_secret = lKv9iRIqRIGKLyYV0fhYfUhNIRTXDEJ/mpFSEJdNJZA=
+
+[frontend]
+locale_path = frontend/locale
+static_path = frontend/static
+template_path = frontend/template
+
+[sqlite]
+enabled = yes
+database = :memory:
+
+[redis]
+enabled = yes
+# unixsocket = /tmp/redis.sock
+host = 127.0.0.1
+port = 6379
+dbid = 0
+poolsize = 10
+
+[mysql]
+enabled = yes
+host = 127.0.0.1
+port = 3306
+username = foo
+password = bar
+database = dummy
+poolsize = 10
+debug = yes
+
+[email]
+enabled = yes
+host = smtp.gmail.com
+port = 587
+tls = yes
+username = foo
+password = bar
View
13 appskel/signup/modname.sql
@@ -0,0 +1,13 @@
+drop database if exists dummy;
+create database dummy;
+grant all privileges on dummy.* to 'foo'@'localhost' identified by 'bar';
+use dummy;
+
+create table users (
+ id integer not null auto_increment,
+ user_email varchar(50) not null,
+ user_passwd varchar(40) not null,
+ user_full_name varchar(80) null,
+ user_is_active boolean not null,
+ primary key(id)
+);
View
6 appskel/signup/modname/__init__.py
@@ -0,0 +1,6 @@
+# coding: utf-8
+#
+$license
+
+__author__ = "$name <$email>"
+__version__ = "$version"
View
92 appskel/signup/modname/config.py
@@ -0,0 +1,92 @@
+# coding: utf-8
+#
+$license
+
+import os
+import sys
+import ConfigParser
+
+from cyclone.util import ObjectDict
+
+
+def tryget(func, section, option, default=None):
+ try:
+ return func(section, option)
+ except ConfigParser.NoOptionError:
+ return default
+
+
+def my_parse_config(filename):
+ cp = ConfigParser.RawConfigParser()
+ cp.read([filename])
+
+ conf = dict(raw=cp, config_file=filename)
+
+ # server settings
+ conf["debug"] = tryget(cp.getboolean, "server", "debug", False)
+ conf["xheaders"] = tryget(cp.getboolean, "server", "xheaders", False)
+ conf["cookie_secret"] = cp.get("server", "cookie_secret")
+ conf["xsrf_cookies"] = tryget(cp.getboolean,
+ "server", "xsrf_cookies", False)
+
+ # make relative path absolute to this file's parent directory
+ root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ getpath = lambda k, v: os.path.join(root, tryget(cp.get, k, v))
+
+ # locale, template and static directories
+ conf["locale_path"] = getpath("frontend", "locale_path")
+ conf["static_path"] = getpath("frontend", "static_path")
+ conf["template_path"] = getpath("frontend", "template_path")
+
+ # sqlite support
+ if tryget(cp.getboolean, "sqlite", "enabled", False) is True:
+ conf["sqlite_settings"] = \
+ ObjectDict(database=cp.get("sqlite", "database"))
+
+ # redis support
+ if tryget(cp.getboolean, "redis", "enabled", False) is True:
+ conf["redis_settings"] = ObjectDict(
+ unixsocket=tryget(cp.get, "redis", "unixsocket", None),
+ host=tryget(cp.get, "redis", "host", "127.0.0.1"),
+ port=tryget(cp.getint, "redis", "port", 6379),
+ dbid=tryget(cp.getint, "redis", "dbid", 0),
+ poolsize=tryget(cp.getint, "redis", "poolsize", 10))
+ else:
+ raise ValueError("Redis is mandatory, but is currently disabled "
+ "in $modname.conf. Not running.")
+
+ # mysql support
+ if tryget(cp.getboolean, "mysql", "enabled", False) is True:
+ conf["mysql_settings"] = ObjectDict(
+ host=cp.get("mysql", "host"),
+ port=cp.getint("mysql", "port"),
+ username=tryget(cp.get, "mysql", "username"),
+ password=tryget(cp.get, "mysql", "password"),
+ database=tryget(cp.get, "mysql", "database"),
+ poolsize=tryget(cp.getint, "mysql", "poolsize", 10),
+ debug=tryget(cp.getboolean, "mysql", "debug", False))
+ else:
+ raise ValueError("MySQL is mandatory, but is currently disabled "
+ "in $modname.conf. Not running.")
+
+ # email support
+ if tryget(cp.getboolean, "email", "enabled", False) is True:
+ conf["email_settings"] = ObjectDict(
+ host=cp.get("email", "host"),
+ port=tryget(cp.getint, "email", "port"),
+ tls=tryget(cp.getboolean, "email", "tls"),
+ username=tryget(cp.get, "email", "username"),
+ password=tryget(cp.get, "email", "password"))
+ else:
+ raise ValueError("Email is mandatory, but is currently disabled "
+ "in $modname.conf. Not running.")
+
+ return conf
+
+
+def parse_config(filename):
+ try:
+ return my_parse_config(filename)
+ except Exception, e:
+ print("Error parsing %s: %s" % (filename, e))
+ sys.exit(1)
View
94 appskel/signup/modname/storage.py
@@ -0,0 +1,94 @@
+# coding: utf-8
+#
+$license
+
+try:
+ sqlite_ok = True
+ import cyclone.sqlite
+except ImportError, sqlite_err:
+ sqlite_ok = False
+
+import MySQLdb
+import cyclone.redis
+import functools
+
+from twisted.internet import defer
+from twisted.python import log
+
+from $modname import txdbapi
+
+
+class users(txdbapi.DatabaseModel):
+ pass
+
+
+def DatabaseSafe(method):
+ """This decorator function makes all database calls safe from connection
+ errors. It returns an HTTP 503 when either redis or mysql are temporarily
+ disconnected.
+
+ @database_safe
+ def get(self):
+ now = yield self.mysql.runQuery("select now()")
+ print now
+ """
+ @defer.inlineCallbacks
+ @functools.wraps(method)
+ def run(self, *args, **kwargs):
+ try:
+ r = yield defer.maybeDeferred(method, self, *args, **kwargs)
+ except cyclone.redis.ConnectionError, e:
+ m = "redis.ConnectionError: %s" % e
+ log.msg(m)
+ raise cyclone.web.HTTPError(503, m) # Service Unavailable
+ except (MySQLdb.InterfaceError, MySQLdb.OperationalError), e:
+ m = "mysql.Error: %s" % e
+ log.msg(m)
+ raise cyclone.web.HTTPError(503, m) # Service Unavailable
+ else:
+ defer.returnValue(r)
+
+ return run
+
+
+class DatabaseMixin(object):
+ mysql = None
+ redis = None
+ sqlite = None
+
+ @classmethod
+ def setup(cls, conf):
+ if "sqlite_settings" in conf:
+ if sqlite_ok:
+ DatabaseMixin.sqlite = \
+ cyclone.sqlite.InlineSQLite(conf["sqlite_settings"].database)
+ else:
+ log.err("SQLite is currently disabled: %s" % sqlite_err)
+
+ if "redis_settings" in conf:
+ if conf["redis_settings"].get("unixsocket"):
+ DatabaseMixin.redis = \
+ cyclone.redis.lazyUnixConnectionPool(
+ conf["redis_settings"].unixsocket,
+ conf["redis_settings"].dbid,
+ conf["redis_settings"].poolsize)
+ else:
+ DatabaseMixin.redis = \
+ cyclone.redis.lazyConnectionPool(
+ conf["redis_settings"].host,
+ conf["redis_settings"].port,
+ conf["redis_settings"].dbid,
+ conf["redis_settings"].poolsize)
+
+ if "mysql_settings" in conf:
+ txdbapi.DatabaseModel.db = DatabaseMixin.mysql = \
+ txdbapi.ConnectionPool("MySQLdb",
+ host=conf["mysql_settings"].host,
+ port=conf["mysql_settings"].port,
+ db=conf["mysql_settings"].database,
+ user=conf["mysql_settings"].username,
+ passwd=conf["mysql_settings"].password,
+ cp_min=1,
+ cp_max=conf["mysql_settings"].poolsize,
+ cp_reconnect=True,
+ cp_noisy=conf["mysql_settings"].debug)
View
325 appskel/signup/modname/txdbapi.py
@@ -0,0 +1,325 @@
+# coding: utf-8
+# http://en.wikipedia.org/wiki/Active_record_pattern
+# http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
+#
+$license
+
+import sqlite3
+import sys
+import types
+
+from twisted.enterprise import adbapi
+from twisted.internet import defer
+
+
+class InlineSQLite:
+ def __init__(self, dbname, autocommit=True, cursorclass=None):
+ self.autocommit = autocommit
+ self.conn = sqlite3.connect(dbname)
+ if cursorclass:
+ self.conn.row_factory = cursorclass
+
+ self.curs = self.conn.cursor()
+
+ def runQuery(self, query, *args, **kwargs):
+ self.curs.execute(query.replace("%s", "?"), *args, **kwargs)
+ return self.curs.fetchall()
+
+ def runOperation(self, command, *args, **kwargs):
+ self.curs.execute(command.replace("%s", "?"), *args, **kwargs)
+ if self.autocommit is True:
+ self.conn.commit()
+
+ def runOperationMany(self, command, *args, **kwargs):
+ self.curs.executemany(command.replace("%s", "?"), *args, **kwargs)
+ if self.autocommit is True:
+ self.conn.commit()
+
+ def runInteraction(self, interaction, *args, **kwargs):
+ return interaction(self.curs, *args, **kwargs)
+
+ def commit(self):
+ self.conn.commit()
+
+ def rollback(self):
+ self.conn.rollback()
+
+ def close(self):
+ self.conn.close()
+
+
+def ConnectionPool(dbapiName, *args, **kwargs):
+ if dbapiName == "sqlite3":
+ if sys.version_info < (2, 6):
+ # hax for py2.5
+ def __row(cursor, row):
+ d = {}
+ for idx, col in enumerate(cursor.description):
+ d[col[0]] = row[idx]
+ return d
+
+ kwargs["cursorclass"] = __row
+ else:
+ kwargs["cursorclass"] = sqlite3.Row
+
+ return InlineSQLite(*args, **kwargs)
+
+ elif dbapiName == "MySQLdb":
+ import MySQLdb.cursors
+ kwargs["cursorclass"] = MySQLdb.cursors.DictCursor
+ return adbapi.ConnectionPool(dbapiName, *args, **kwargs)
+
+ elif dbapiName == "psycopg2":
+ import psycopg2
+ import psycopg2.extras
+ psycopg2.connect = psycopg2.extras.RealDictConnection
+ return adbapi.ConnectionPool(dbapiName, *args, **kwargs)
+
+ else:
+ raise ValueError("Database %s is not yet supported." % dbapiName)
+
+
+class DatabaseObject(object):
+ def __init__(self, model, row):
+ self._model = model
+ self._changes = set()
+ self._data = {}
+ for k, v in dict(row).items():
+ self.__setattr__(k, v)
+
+ def __setattr__(self, k, v):
+ if k[0] == "_":
+ object.__setattr__(self, k, v)
+ else:
+ if k in self._data:
+ self._changes.add(k)
+
+ if k in self._model.codecs and \
+ not isinstance(v, types.StringTypes):
+ self._data[k] = self._model.codecs[k][0](v)
+ else:
+ self._data[k] = v
+
+ def __getattr__(self, k):
+ if [0] == "_":
+ object.__getattr__(self, k)
+ else:
+ return self._model.codecs[k][1](self._data[k]) \
+ if k in self._model.codecs else self._data[k]
+
+ def __setitem__(self, k, v):
+ self.__setattr__(k, v)
+
+ def __getitem__(self, k):
+ return self.__getattr__(k)
+
+ def get(self, k, default=None):
+ return self._data.get(k, default)
+
+ @property
+ def has_changes(self):
+ return bool(self._changes)
+
+ @defer.inlineCallbacks
+ def save(self, force=False):
+ if "id" in self._data:
+ if self._changes and not force:
+ kv = dict(map(lambda k: (k, self._data[k]), self._changes))
+ kv["where"] = ("id=%s", self._data["id"])
+ yield self._model.update(**kv)
+ elif force:
+ k, v = self._data.items()
+ yield self._model.update(set=(k, v),
+ where=("id=%s", self._data["id"]))
+
+ self._changes.clear()
+ defer.returnValue(self)
+ else:
+ rs = yield self._model.insert(**self._data)
+ self["id"] = rs["id"]
+ defer.returnValue(self)
+
+ @defer.inlineCallbacks
+ def delete(self):
+ if "id" in self._data:
+ yield self._model.delete(where=("id=%s", self._data["id"]))
+ self._data.pop("id")
+
+ defer.returnValue(self)
+
+ def __repr__(self):
+ return repr(self._data)
+
+
+class DatabaseCRUD(object):
+ #db = None
+ allow = []
+ deny = []
+ codecs = {}
+
+ @classmethod
+ def __table__(cls):
+ return getattr(cls, "table_name", cls.__name__)
+
+ @classmethod
+ def kwargs_cleanup(cls, kwargs):
+ if cls.allow:
+ deny = cls.deny + [k for k in kwargs if k not in cls.allow]
+ else:
+ deny = cls.deny
+
+ if deny:
+ map(lambda k: kwargs.pop(k, None), deny)
+
+ return kwargs
+
+ @classmethod
+ @defer.inlineCallbacks
+ def insert(cls, **kwargs):
+ kwargs = cls.kwargs_cleanup(kwargs)
+
+ keys = kwargs.keys()
+ q = "insert into %s (%s) values " % (cls.__table__(),
+ ",".join(keys)) + "(%s)"
+
+ vs = []
+ vd = []
+ for v in kwargs.itervalues():
+ vs.append("%s")
+ vd.append(v["id"] if isinstance(v, DatabaseObject) else v)
+
+ if isinstance(cls.db, InlineSQLite):
+ vs = [s.replace("%s", "?") for s in vs]
+
+ q = q % ",".join(vs)
+
+ if "id" in kwargs:
+ yield cls.db.runOperation(q, vd)
+ else:
+ def _insert_transaction(trans, *args, **kwargs):
+ trans.execute(*args, **kwargs)
+ if isinstance(cls.db, InlineSQLite):
+ trans.execute("select last_insert_rowid() as id")
+ elif cls.db.dbapiName == "MySQLdb":
+ trans.execute("select last_insert_id() as id")
+ elif cls.db.dbapiName == "psycopg2":
+ trans.execute("select currval('%s_id_seq') as id" %
+ cls.__table__())
+ return trans.fetchall()
+
+ r = yield cls.db.runInteraction(_insert_transaction, q, vd)
+ kwargs["id"] = r[0]["id"]
+
+ defer.returnValue(DatabaseObject(cls, kwargs))
+
+ @classmethod
+ def update(cls, **kwargs):
+ where = kwargs.pop("where", None)
+ kwargs = cls.kwargs_cleanup(kwargs)
+
+ keys = kwargs.keys()
+ vals = [kwargs[k] for k in keys]
+ keys = ",".join(["%s=%s" % (k, "%s") for k in keys])
+
+ if where:
+ where, args = where[0], list(where[1:])
+ for arg in args:
+ if isinstance(arg, DatabaseObject):
+ vals.append(arg["id"])
+ else:
+ vals.append(arg)
+
+ return cls.db.runOperation("update %s set %s where %s" %
+ (cls.__table__(), keys, where), vals)
+ else:
+ return cls.db.runOperation("update %s set %s" %
+ (cls.__table__(), keys), vals)
+
+ @classmethod
+ @defer.inlineCallbacks
+ def select(cls, **kwargs):
+ extra = []
+ star = "id,*" if isinstance(cls.db, InlineSQLite) else "*"
+
+ if "groupby" in kwargs:
+ extra.append("group by %s" % kwargs["groupby"])
+
+ if "orderby" in kwargs:
+ extra.append("order by %s" % kwargs["orderby"])
+
+ if "asc" in kwargs and kwargs["asc"] is True:
+ extra.append("asc")
+
+ if "desc" in kwargs and kwargs["desc"] is True:
+ extra.append("desc")
+
+ if "limit" in kwargs:
+ extra.append("limit %s" % kwargs["limit"])
+
+ if "offset" in kwargs:
+ extra.append("offset %s" % kwargs["offset"])
+
+ extra = " ".join(extra)
+
+ if "where" in kwargs:
+ where, args = kwargs["where"][0], list(kwargs["where"][1:])
+ for n, arg in enumerate(args):
+ if isinstance(arg, DatabaseObject):
+ args[n] = arg["id"]
+
+ rs = yield cls.db.runQuery("select %s from %s where %s %s" %
+ (star, cls.__table__(), where, extra),
+ args)
+ else:
+ rs = yield cls.db.runQuery("select %s from %s %s" %
+ (star, cls.__table__(), extra))
+
+ result = map(lambda d: DatabaseObject(cls, d), rs)
+ defer.returnValue(result)
+
+ @classmethod
+ def delete(cls, **kwargs):
+ if "where" in kwargs:
+ where, args = kwargs["where"][0], kwargs["where"][1:]
+ return cls.db.runOperation("delete from %s where %s" %
+ (cls.__table__(), where), args)
+ else:
+ return cls.db.runOperation("delete from %s" % cls.__table__())
+
+ def __str__(self):
+ return str(self.data)
+
+
+class DatabaseModel(DatabaseCRUD):
+ @classmethod
+ @defer.inlineCallbacks
+ def count(cls, **kwargs):
+ if "where" in kwargs:
+ where, args = kwargs["where"][0], kwargs["where"][1:]
+ rs = yield cls.db.runQuery("select count(*) as count from %s"
+ "where %s" %
+ (cls.__table__(), where), args)
+ else:
+ rs = yield cls.db.runQuery("select count(*) as count from %s" %
+ cls.__table__())
+
+ defer.returnValue(rs[0]["count"])
+
+ @classmethod
+ def all(cls):
+ return cls.select()
+
+ @classmethod
+ def find(cls, **kwargs):
+ return cls.select(**kwargs)
+
+ @classmethod
+ @defer.inlineCallbacks
+ def find_first(cls, **kwargs):
+ kwargs["limit"] = 1
+ rs = yield cls.select(**kwargs)
+ defer.returnValue(rs[0] if rs else None)
+
+ @classmethod
+ def new(cls, **kwargs):
+ return DatabaseObject(cls, kwargs)
View
125 appskel/signup/modname/utils.py
@@ -0,0 +1,125 @@
+# coding: utf-8
+#
+$license
+
+import OpenSSL
+import cyclone.escape
+import cyclone.web
+import httplib
+import re
+import uuid
+
+from twisted.internet import defer
+
+from $modname.storage import DatabaseMixin
+
+
+class TemplateFields(dict):
+ """Helper class to make sure our
+ template doesn't fail due to an invalid key"""
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ return None
+
+ def __setattr__(self, name, value):
+ self[name] = value
+
+
+class BaseHandler(cyclone.web.RequestHandler):
+ _email = re.compile("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,8}$$")
+
+ def valid_email(self, email):
+ return self._email.match(email)
+
+ def set_current_user(self, expires_days=1, **kwargs):
+ self.set_secure_cookie("user", cyclone.escape.json_encode(kwargs),
+ expires_days=expires_days)
+
+ def get_current_user(self):
+ user_json = self.get_secure_cookie("user", max_age_days=1)
+ if user_json:
+ return cyclone.escape.json_decode(user_json)
+
+ def clear_current_user(self):
+ self.clear_cookie("user")
+
+ def get_user_locale(self):
+ lang = self.get_secure_cookie("lang")
+ if lang:
+ return cyclone.locale.get(lang)
+
+ # custom http error pages
+ def write_error(self, status_code, **kwargs):
+ kwargs["code"] = status_code
+ kwargs["message"] = httplib.responses[status_code]
+ try:
+ self.render("error_%d.html" % status_code, fields=kwargs)
+ except IOError:
+ self.render("error_all.html", fields=kwargs)
+
+
+class SessionMixin(DatabaseMixin):
+ session_cookie_name = "session"
+ session_redis_prefix = "$modname:s:"
+
+ @property
+ def session_redis_key(self):
+ token = self.get_secure_cookie(self.session_cookie_name)
+ if token:
+ return "%s%s" % (self.session_redis_prefix, token)
+
+ @defer.inlineCallbacks
+ def session_create(self, expires_days=1, **kwargs):
+ if not kwargs:
+ raise ValueError("session_create requires one or more key=val")
+
+ token = uuid.UUID(bytes=OpenSSL.rand.bytes(16)).hex
+ k = "%s%s" % (self.session_redis_prefix, token)
+
+ yield self.redis.hmset(k, kwargs)
+ yield self.redis.expire(k, expires_days * 86400)
+
+ self.set_secure_cookie(self.session_cookie_name, token,
+ expires_days=expires_days)
+ defer.returnValue(token)
+
+ @defer.inlineCallbacks
+ def session_exists(self):
+ k = self.session_redis_key
+ if k:
+ defer.returnValue((yield self.redis.exists(k)))
+
+ @defer.inlineCallbacks
+ def session_set(self, **kwargs):
+ if not kwargs:
+ raise ValueError("session_set requires one or more key=val")
+
+ k = self.session_redis_key
+ if k:
+ yield self.redis.hmset(k, kwargs)
+ defer.returnValue(True)
+
+ @defer.inlineCallbacks
+ def session_get(self, *args):
+ if not args:
+ raise ValueError("session_get requires one or more key names")
+
+ k = self.session_redis_key
+ if k:
+ r = yield self.redis.hmget(k, args)
+ defer.returnValue(r[0] if len(args) == 1 else r)
+
+ @defer.inlineCallbacks
+ def session_getall(self):
+ k = self.session_redis_key
+ if k:
+ defer.returnValue((yield self.redis.hgetall(k)))
+
+ @defer.inlineCallbacks
+ def session_destroy(self):
+ k = self.session_redis_key
+ if k:
+ yield self.redis.delete(k)
+ defer.returnValue(True)
View
331 appskel/signup/modname/views.py
@@ -0,0 +1,331 @@
+# coding: utf-8
+#
+$license
+
+import OpenSSL
+import cyclone.escape
+import cyclone.locale
+import cyclone.mail
+import cyclone.web
+import hashlib
+import random
+import string
+
+from datetime import datetime
+
+from twisted.internet import defer
+from twisted.python import log
+
+from $modname import storage
+from $modname.utils import BaseHandler
+from $modname.utils import SessionMixin
+from $modname.utils import TemplateFields
+
+
+class IndexHandler(BaseHandler, SessionMixin):
+ def get(self):
+ if self.current_user:
+ self.redirect("/dashboard")
+ else:
+ self.render("index.html")
+
+
+class LangHandler(BaseHandler):
+ def get(self, lang_code):
+ if lang_code in cyclone.locale.get_supported_locales():
+ self.set_secure_cookie("lang", lang_code, expires_days=20)
+
+ self.redirect(self.request.headers.get("Referer",
+ self.get_argument("next", "/")))
+
+
+class DashboardHandler(BaseHandler):
+ @cyclone.web.authenticated
+ def get(self):
+ self.render("dashboard.html")
+
+
+class AccountHandler(BaseHandler, storage.DatabaseMixin):
+ @cyclone.web.authenticated
+ @storage.DatabaseSafe
+ @defer.inlineCallbacks
+ def get(self):
+ user = yield storage.users.find_first(
+ where=("user_email=%s", self.current_user["email"]))
+
+ if user:
+ self.render("account.html",
+ fields=TemplateFields(full_name=user["user_full_name"]))
+ else:
+ self.clear_current_user()
+ self.redirect("/")
+
+ @cyclone.web.authenticated
+ @storage.DatabaseSafe
+ @defer.inlineCallbacks
+ def post(self):
+ user = yield storage.users.find_first(
+ where=("user_email=%s", self.current_user["email"]))
+ if not user:
+ self.clear_current_user()
+ self.redirect("/")
+ defer.returnValue(None)
+
+ full_name = self.get_argument("full_name", None)
+ f = TemplateFields(full_name=full_name)
+
+ if full_name:
+ full_name = full_name.strip()
+ if len(full_name) > 80:
+ f["err"] = ["invalid_name"]
+ self.render("account.html", fields=f)
+ defer.returnValue(None)
+ elif full_name != user.user_full_name:
+ user.user_full_name = full_name
+
+ passwd_1 = self.get_argument("passwd_1", None)
+ passwd_2 = self.get_argument("passwd_2", None)
+ if passwd_1:
+ if len(passwd_1) < 3 or len(passwd_1) > 20:
+ f["err"] = ["invalid_passwd"]
+ self.render("account.html", fields=f)
+ defer.returnValue(None)
+ elif passwd_1 != passwd_2:
+ f["err"] = ["nomatch"]
+ self.render("account.html", fields=f)
+ defer.returnValue(None)
+ else:
+ user.user_passwd = hashlib.sha1(passwd_1).hexdigest()
+
+ if user.has_changes:
+ yield user.save()
+ f["updated"] = True
+
+ self.render("account.html", fields=f)
+
+
+class SignUpHandler(BaseHandler, storage.DatabaseMixin):
+ def get(self):
+ if self.get_current_user():
+ self.redirect("/")
+ else:
+ self.render("signup.html", fields=TemplateFields())
+
+ @storage.DatabaseSafe
+ @defer.inlineCallbacks
+ def post(self):
+ email = self.get_argument("email", None)
+ legal = self.get_argument("legal", None)
+
+ f = TemplateFields(email=email, legal=legal)
+
+ if legal != "on":
+ f["err"] = ["legal"]
+ self.render("signup.html", fields=f)
+ defer.returnValue(None)
+
+ if not email:
+ f["err"] = ["email"]
+ self.render("signup.html", fields=f)
+ defer.returnValue(None)
+
+ if not self.valid_email(email):
+ f["err"] = ["email"]
+ self.render("signup.html", fields=f)
+ defer.returnValue(None)
+
+ # check if the email is awaiting confirmation
+ if (yield self.redis.exists("u:%s" % email)):
+ f["err"] = ["exists"]
+ self.render("signup.html", fields=f)
+ defer.returnValue(None)
+
+ # check if the email exists in the database
+ if (yield storage.users.find_first(where=("user_email=%s", email))):
+ f["err"] = ["exists"]
+ self.render("signup.html", fields=f)
+
+ # create random password
+ random.seed(OpenSSL.rand.bytes(16))
+ passwd = "".join(random.choice(string.letters + string.digits)
+ for x in range(8))
+
+ # store temporary password in redis
+ k = "u:%s" % email
+ t = yield self.redis.multi()
+ t.set(k, passwd)
+ t.expire(k, 86400) # 1 day
+ yield t.commit()
+
+ # prepare the confirmation email
+ msg = cyclone.mail.Message(
+ mime="text/html",
+ charset="utf-8",
+ to_addrs=[email],
+ from_addr=self.settings.email_settings.username,
+ subject=self.render_string("signup_email_subject.txt")
+ .replace("\n", "").strip(),
+ message=self.render_string("signup_email.html",
+ passwd=passwd, ip=self.request.remote_ip,
+ date=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S GMT")))
+
+ try:
+ r = yield cyclone.mail.sendmail(self.settings.email_settings, msg)
+ except Exception, e:
+ # delete password from redis
+ yield self.redis.delete(k)
+
+ log.err("failed to send signup email to %s: %s" % (email, e))
+ f["err"] = ["send"]
+ self.render("signup.html", fields=f)
+ else:
+ log.msg("signup email sent to %s: %s" % (email, r))
+ self.render("signup_ok.html", email=email)
+
+
+class SignInHandler(BaseHandler, storage.DatabaseMixin):
+ def get(self):
+ if self.get_current_user():
+ self.redirect("/")
+ else:
+ self.render("signin.html", fields=TemplateFields())
+
+ @storage.DatabaseSafe
+ @defer.inlineCallbacks
+ def post(self):
+ email = self.get_argument("email", "")
+ passwd = self.get_argument("passwd", "")
+ remember = self.get_argument("remember", "")
+
+ f = TemplateFields(email=email, remember=remember)
+
+ if not email:
+ f["err"] = ["auth"]
+ self.render("signin.html", fields=f)