commit 80cdb11749afa9d2ecfcbb0a91f3f867f183bfc3 Author: Kris Lamoureux Date: Sat Oct 13 07:35:48 2012 -0700 First Commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0ffd10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +############ +## Windows +############ + +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Database +*.db + +# Batch +Py2Start_SetupProject.bat +DjangoBatchManager.bat + +# My personal config +config_.txt + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd795f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2012, Kris Lamoureux +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name DJ-BaseSite nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..fe4032b --- /dev/null +++ b/README @@ -0,0 +1,31 @@ +DJ-BaseSite was written in Python 2.7 and Django 1.4 + +Description: +DJ-BaseSite (or Django-BaseSite) is a customizable login and register system with required email activation. While most people just use django-registration, I decided to write my own to learn more about Django. + +NOTE TO DEVS: +A deactivation system hasn't been added. The login system needs to check attempts and display a CAPTCHA after a certain amount, this isn't included either. It also doesn't have an account recovery option. I plan to implement all of these things in future updates. + +DJ-BaseSite is released under the New BSD License (The BSD 3-Clause License), refer to the LICENSE file. + + +Instructions +1. Install Python 2.7 (python.org) and Django 1.4 (djangoproject.com) +2. Open up the config.txt file and change the data under CUSTOM VARIABLES to your information. The configuration variable meanings are below. +3. Execute the SetupProject.py script and enter the project name, it will create a project based on your configuration. +4. Run the SyncDB script or "python manage.py syncdb" and create a super user. +5. Run the RunServer script or "python manage.py runserver" and check out your new website at localhost:8000 or 127.0.0.1:8000 in your browser. + +DJ-BaseSite uses Recaptcha to prevent bots from creating accounts, so you'll need to get private and public keys from the website: http://www.google.com/recaptcha + +Configuration Variable Meanings. + +baseurl - It is used to create activation and deactivation links ( in views.py in register_user() ) +admin_name/email - adds a name and email to the ADMINS tuple in settings.py ( http://docs.djangoproject.com/en/dev/ref/settings/#admins ) +secret_key - A secret randomly generated string of characters (don't use spaces) ( http://www.random.org/passwords/?mode=advanced ) +captcha_publickey/privatekey - DJ-BaseSite uses Recaptcha to prevent bots from creating accounts, get keys at google.com/recaptcha +HOSTsmpt - The SMPT server address with the email used to send activation emails. If your email doesn't support SMPT I highly suggest GMAIL. +HOSTemail - The email address +HOSTpass - The email's password + +NOTE: It creates a development project, DEBUG is set to True in the settings.py so it is in no condition for deployment. \ No newline at end of file diff --git a/SetupProject.py b/SetupProject.py new file mode 100644 index 0000000..06b1300 --- /dev/null +++ b/SetupProject.py @@ -0,0 +1,132 @@ +''' + +Copyright (c) 2012, Kris Lamoureux +All rights reserved. + +Released under the New BSD. + +''' + +from os import mkdir +from shutil import copy +import os.path +import traceback + +PROJECT_HOME = "myproject" +PROJECT = "" + +HERE = os.path.dirname(os.path.realpath(__file__)) +HERE = HERE.replace('\\','/') + +REPLACEM = [] + +def add_REPLACE(a): + global REPLACEM + REPLACEM.append(a) + + +def main(): + global PROJECT, PROJECT_HOME, HERE, REPLACEM + PROJECT = raw_input("New Project Name: ") + add_REPLACE(['<%myproject%>',PROJECT]) + mkdir(PROJECT) + mkdir(PROJECT+'/'+PROJECT) + + files = [] + move = [] + + + ''' + The config system is a little picky, it works + but it can be written better, and I plan to + update it. + ''' + f_hand = open("./config.txt") + config_list = f_hand.readlines() + + for config_setting in config_list: + # take off '\n' at end of string + config_setting = config_setting[:-1] + + # ignore comments. + if config_setting[0:2] == "//": + pass + # ignore empty lines + elif config_setting == "": + pass + # ignore newlines + elif config_setting == "\n": + pass + # add file variable. (format: <%myproject%>) + elif config_setting[0:1] == "v": + config_setting = config_setting.split(' ') + config_setting[1] = "<%"+config_setting[1]+"%>" + try: + add_REPLACE([config_setting[1],config_setting[2]]) + except IndexError: + print "You have an empty variable in the configuration." + except: + traceback.print_exc() + raw_input() + quit() + + # Create folder for an app. + elif config_setting[0:3] == "dir": + config_setting = config_setting.split(' ') + if not len(config_setting) == 3: + mkdir(HERE+'/'+PROJECT+'/'+PROJECT+'/'+config_setting[1]) + else: + mkdir(HERE+'/'+PROJECT+'/'+config_setting[1]) + else: + old_path = config_setting.split(' ')[0] + old_path= old_path.replace("%here%",HERE) + new_path = old_path.replace("myproject", PROJECT) + + if config_setting.split(' ')[1] == '1': + files.append([old_path, new_path]) + elif config_setting.split(' ')[1] == '2': + move.append([old_path, new_path]) + else: + print "Something went wrong here.." + raw_input() + exit() + ''' + END OF CONFIG SYSTEM. + ''' + + + # Cycle through files, dynamic then static. + for dyn_file in files: + # Open file that has vars to replace (format: <%varname%>) + + old_fileh = open(dyn_file[0]) + old_file = old_fileh.read() + + # Replace vars with data + for var in REPLACEM: + old_file = old_file.replace(var[0],var[1]) + + # Write new file + f = open(dyn_file[1], "w+") + f.write(old_file) + f.close() + + # Static cycle. + stat_file_dir = "" + for sta_file in move: + if sta_file[1].find('.'): + stat_file_dir = os.path.dirname(sta_file[1]) + copy(sta_file[0], stat_file_dir) + else: + copy(sta_file[0], sta_file[1]) + + print "Project: "+PROJECT+" is ready for development.\n" +try: + main() +except: + traceback.print_exc() + +print "Press any key to quit." +raw_input() +quit() + \ No newline at end of file diff --git a/config.txt b/config.txt new file mode 100644 index 0000000..b35eeb2 --- /dev/null +++ b/config.txt @@ -0,0 +1,95 @@ +// Copyright (c) 2012, Kris Lamoureux +// All rights reserved. + +// DJ-BaseSite is released under the New BSD License +// Read the LICENSE file. + + +// Put all comments on new lines on the first 2 +// characters of the line. + +// CUSTOM VARIABLES +v baseurl http://127.0.0.1:8000 + +v admin_name Kris619 +v admin_email KrisPublicEmail@gmail.com + + +// Change this to something random and secret +// http://www.random.org/passwords/?mode=advanced +v secret_key WK9zRDBQCUSqzU64Jf5sxta4X9TnKDPHhuXwA7Zj + + +// Get free captcha keys here: google.com/recaptcha +v captcha_publickey asdf +v captcha_privatekey asdf + +v HOSTsmtp smtp.gmail.com +v HOSTemail KrisPublicEmail@gmail.com +v HOSTpass HereItIs_HaveMyAccount + + + + + + + + + +// !!!!!!HEY, PAY ATTENTION FOR A SECOND!!!!!! +// Don't mess with anything below unless you +// know what you are doing. + +// DYNAMIC FILES +%here%/myproject/manage.py 1 +%here%/myproject/myproject/wsgi.py 1 +%here%/myproject/myproject/settings.py 1 +%here%/myproject/myproject/urls.py 1 +%here%/myproject/manage.py 1 + +// STATIC FILES +%here%/myproject/myproject/__init__.py 2 +%here%/myproject/myproject/views.py 2 +%here%/myproject/myproject/validation.py 2 +%here%/myproject/myproject/captcha.py 2 +%here%/myproject/RunServer.bat 2 +%here%/myproject/SyncDB.bat 2 + +// Activation Email +%here%/myproject/myproject/activation_email.html 2 + +// Add these dir commands before actually adding +// the files from inside the folders. + +// HTML/CSS +dir static +dir static/css +dir templates +%here%/myproject/myproject/static/css/default.css 2 +%here%/myproject/myproject/templates/base.html 2 +%here%/myproject/myproject/templates/index.html 2 +%here%/myproject/myproject/templates/error.html 2 + +// Auth +dir templates/auth +%here%/myproject/myproject/templates/auth/activated.html 2 +%here%/myproject/myproject/templates/auth/disabled.html 2 +%here%/myproject/myproject/templates/auth/logged_in.html 2 +%here%/myproject/myproject/templates/auth/logged_out.html 2 +%here%/myproject/myproject/templates/auth/login.html 2 +%here%/myproject/myproject/templates/auth/newaccount.html 2 +%here%/myproject/myproject/templates/auth/registration.html 2 + +// Admin +dir templates/admin +%here%/myproject/myproject/templates/admin/base_site.html 2 + +// Backend Django app +dir backends root +%here%/myproject/backends/__init__.py 2 +%here%/myproject/backends/AuthOverride.py 2 + +// Profile Django app +dir accountprofile root +%here%/myproject/accountprofile/__init__.py 2 +%here%/myproject/accountprofile/models.py 2 diff --git a/myproject/RunServer.bat b/myproject/RunServer.bat new file mode 100644 index 0000000..97e4146 --- /dev/null +++ b/myproject/RunServer.bat @@ -0,0 +1 @@ +@python manage.py runserver diff --git a/myproject/SyncDB.bat b/myproject/SyncDB.bat new file mode 100644 index 0000000..374476f --- /dev/null +++ b/myproject/SyncDB.bat @@ -0,0 +1 @@ +@python manage.py syncdb diff --git a/myproject/accountprofile/__init__.py b/myproject/accountprofile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/accountprofile/models.py b/myproject/accountprofile/models.py new file mode 100644 index 0000000..bf75eb7 --- /dev/null +++ b/myproject/accountprofile/models.py @@ -0,0 +1,7 @@ +from django.db import models +from django.contrib.auth.models import User + +class UserProfile(models.Model): + user = models.ForeignKey(User, unique=True) + activated = models.BooleanField() + activatekey = models.CharField(max_length=25, blank=True) \ No newline at end of file diff --git a/myproject/backends/AuthOverride.py b/myproject/backends/AuthOverride.py new file mode 100644 index 0000000..e522cdd --- /dev/null +++ b/myproject/backends/AuthOverride.py @@ -0,0 +1,13 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import User + +class CaseInsensitiveModelBackend(ModelBackend): + def authenticate(self, username=None, password=None): + try: + user = User.objects.get(username__iexact=username) + if user.check_password(password): + return user + else: + return None + except User.DoesNotExist: + return None \ No newline at end of file diff --git a/myproject/backends/__init__.py b/myproject/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/manage.py b/myproject/manage.py new file mode 100644 index 0000000..7b2398d --- /dev/null +++ b/myproject/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "<%myproject%>.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/myproject/myproject/__init__.py b/myproject/myproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/myproject/activation_email.html b/myproject/myproject/activation_email.html new file mode 100644 index 0000000..e32be8e --- /dev/null +++ b/myproject/myproject/activation_email.html @@ -0,0 +1,7 @@ +Hello <$user>, +Welcome to the website. Please activate your account "<$user>" +Activation link: <$activatelink> + +If this is not you, I apologize for any inconveniences and you can disable the account and users will not be able to create another one. + +Disable link: <$disablelink> \ No newline at end of file diff --git a/myproject/myproject/captcha.py b/myproject/myproject/captcha.py new file mode 100644 index 0000000..1213fc4 --- /dev/null +++ b/myproject/myproject/captcha.py @@ -0,0 +1,94 @@ +import urllib2, urllib + +API_SSL_SERVER="https://www.google.com/recaptcha/api" +API_SERVER="http://www.google.com/recaptcha/api" +VERIFY_SERVER="www.google.com" + +class RecaptchaResponse(object): + def __init__(self, is_valid, error_code=None): + self.is_valid = is_valid + self.error_code = error_code + +def displayhtml (public_key, + use_ssl = False, + error = None): + """Gets the HTML to display for reCAPTCHA + + public_key -- The public api key + use_ssl -- Should the request be sent over ssl? + error -- An error message to display (from RecaptchaResponse.error_code)""" + + error_param = '' + if error: + error_param = '&error=%s' % error + + if use_ssl: + server = API_SSL_SERVER + else: + server = API_SERVER + + return """ + + +""" % { + 'ApiServer' : server, + 'PublicKey' : public_key, + 'ErrorParam' : error_param, + } + + +def submit (recaptcha_challenge_field, + recaptcha_response_field, + private_key, + remoteip): + """ + Submits a reCAPTCHA request for verification. Returns RecaptchaResponse + for the request + + recaptcha_challenge_field -- The value of recaptcha_challenge_field from the form + recaptcha_response_field -- The value of recaptcha_response_field from the form + private_key -- your reCAPTCHA private key + remoteip -- the user's ip address + """ + + if not (recaptcha_response_field and recaptcha_challenge_field and + len (recaptcha_response_field) and len (recaptcha_challenge_field)): + return RecaptchaResponse (is_valid = False, error_code = 'incorrect-captcha-sol') + + + def encode_if_necessary(s): + if isinstance(s, unicode): + return s.encode('utf-8') + return s + + params = urllib.urlencode ({ + 'privatekey': encode_if_necessary(private_key), + 'remoteip' : encode_if_necessary(remoteip), + 'challenge': encode_if_necessary(recaptcha_challenge_field), + 'response' : encode_if_necessary(recaptcha_response_field), + }) + + request = urllib2.Request ( + url = "http://%s/recaptcha/api/verify" % VERIFY_SERVER, + data = params, + headers = { + "Content-type": "application/x-www-form-urlencoded", + "User-agent": "reCAPTCHA Python" + } + ) + + httpresp = urllib2.urlopen (request) + + return_values = httpresp.read ().splitlines (); + httpresp.close(); + + return_code = return_values [0] + + if (return_code == "true"): + return RecaptchaResponse (is_valid=True) + else: + return RecaptchaResponse (is_valid=False, error_code = return_values [1]) diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py new file mode 100644 index 0000000..df99213 --- /dev/null +++ b/myproject/myproject/settings.py @@ -0,0 +1,169 @@ +''' + +Copyright (c) 2012, Kris Lamoureux +All rights reserved. + +DJ-BaseSite is released under the New BSD Liscense. +Please take a momemt to read the short 3 Clause LICENSE file. + +''' + +# Django settings for the "<%myproject%>" project. +import os +ROOTDIR = os.getcwd().replace('\\','/') + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +baseurl = "<%baseurl%>" # "example.com" + +''' +You need to sign up at http://recaptcha.net/ for a public/private key to use their CAPTCHA service. +''' + +captcha_publickey = "<%captcha_publickey%>" +captcha_privatekey = "<%captcha_privatekey%>" + +EMAIL_USE_TLS = True +EMAIL_HOST = "<%HOSTsmtp%>" +EMAIL_HOST_USER = "<%HOSTemail%>" +EMAIL_HOST_PASSWORD = "<%HOSTpass%>" +EMAIL_PORT = 587 + +EMAIL_MESSAGE = ROOTDIR + "/<%myproject%>/activation_email.html" + +ADMINS = ( + ("<%admin_name%>", "<%admin_email%>"), +) + + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': ROOTDIR + '/data.db', + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + ROOTDIR + "/<%myproject%>/static/", +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = "<%secret_key%>" + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = '<%myproject%>.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = '<%myproject%>.wsgi.application' + +TEMPLATE_DIRS = ( + ROOTDIR + "/<%myproject%>/templates/", +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'accountprofile' +) + +AUTH_PROFILE_MODULE = "accountprofile.UserProfile" + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +AUTHENTICATION_BACKENDS = ('backends.AuthOverride.CaseInsensitiveModelBackend',) \ No newline at end of file diff --git a/myproject/myproject/static/css/default.css b/myproject/myproject/static/css/default.css new file mode 100644 index 0000000..ef7d2c0 --- /dev/null +++ b/myproject/myproject/static/css/default.css @@ -0,0 +1,113 @@ +/* APPLYS TO ALL TEMPLATES */ +body { + font-family:"Comic Sans MS", cursive, sans-serif; + background-color: #FFFFFF; + color: #000000; + padding: 0; + margin: 0; + } + +.content { + padding: 10px; + text-align: center; + } + +.usernav a:link {color:#FFFFFF;} /* unvisited link */ +.usernav a:visited {color:#FFFFFF;} /* visited link */ +.usernav a:hover {color:#FFFFFF;} /* mouse over link */ +.usernav a:active {color:#FFFFFF;} /* selected link */ + +.globalnav a:link {color:#000000;} /* unvisited link */ +.globalnav a:visited {color:#000000;} /* visited link */ +.globalnav a:hover {color:#000000;} /* mouse over link */ +.globalnav a:active {color:#000000;} /* selected link */ + +.usernav { + text-align: right; + padding: 10px; + background-color: #000000; + } + +.globalnav { + text-align: center; + font-size: 20px; + } + +.humantest { + text-align: center; + } + +/* HOME PAGE CSS */ + + + +/* LOGIN CSS */ +.loginform { + text-align: center; + } + +table.loginform td { + padding: 5px; + } + +.loginout { + font-size: 25px; + } + +.returnlink a:link {color:#000000;} /* unvisited link */ +.returnlink a:visited {color:#000000;} /* visited link */ +.returnlink a:hover {color:#000000;} /* mouse over link */ +.returnlink a:active {color:#000000;} /* selected link */ + +/* REGISTER CSS */ +.register_form { + text-align: center; + } + +table.register_form td { + padding: 5px; + } + +.asterisk { + color: #FF0000; + font-size: 17px; + padding: 3px; + } + +.tagpsnrequire { + padding-top: 10px; + } + +.newaccount { + font-size: 25px; + padding: 10px; +} + +/* ERROR RELATED CSS */ + +.error { + color: #FF0000; + font-size: 25px; + padding: 12px; + text-align: center; + background-color: #000000; + font-family:"Lucida Console", Monaco, monospace + } + +.register_error { + color: #FF0000; + font-size: 18px; + padding: 6px; + text-align: center; + background-color: #000000; + font-family:"Lucida Console", Monaco, monospace + } + +.login_error { + color: #FF0000; + font-size: 18px; + padding: 6px; + text-align: center; + background-color: #000000; + font-family:"Lucida Console", Monaco, monospace + } \ No newline at end of file diff --git a/myproject/myproject/templates/admin/base_site.html b/myproject/myproject/templates/admin/base_site.html new file mode 100644 index 0000000..866660c --- /dev/null +++ b/myproject/myproject/templates/admin/base_site.html @@ -0,0 +1,10 @@ +{% extends "admin/base.html" %} +{% load i18n %} + +{% block title %}{{ title }} | {% trans 'Admin Site' %}{% endblock %} + +{% block branding %} +

{% trans 'Administration' %}

+{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/myproject/myproject/templates/auth/activated.html b/myproject/myproject/templates/auth/activated.html new file mode 100644 index 0000000..2eed814 --- /dev/null +++ b/myproject/myproject/templates/auth/activated.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+ Your account "{{user_name}}" has been activated. + +
+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/disabled.html b/myproject/myproject/templates/auth/disabled.html new file mode 100644 index 0000000..91e7205 --- /dev/null +++ b/myproject/myproject/templates/auth/disabled.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +
Your account has been disabled.
+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/logged_in.html b/myproject/myproject/templates/auth/logged_in.html new file mode 100644 index 0000000..ce1260d --- /dev/null +++ b/myproject/myproject/templates/auth/logged_in.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +
You have been logged in.
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/logged_out.html b/myproject/myproject/templates/auth/logged_out.html new file mode 100644 index 0000000..ee2248d --- /dev/null +++ b/myproject/myproject/templates/auth/logged_out.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +
You have been logged out.
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/login.html b/myproject/myproject/templates/auth/login.html new file mode 100644 index 0000000..1c01188 --- /dev/null +++ b/myproject/myproject/templates/auth/login.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} + +{{LOGIN_ERROR|safe}} + +{% if login_errors %} +
+ Username and password did not match. +
+{% endif %} + +

Login

+
+ {% csrf_token %} + + + + + + + + + + + + +
User
Pass
+
+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/newaccount.html b/myproject/myproject/templates/auth/newaccount.html new file mode 100644 index 0000000..ef891dd --- /dev/null +++ b/myproject/myproject/templates/auth/newaccount.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +
+ Account "{{accountname}}" successfully created. Check your email to activate it so you can start using the features on the site. +
+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/auth/registration.html b/myproject/myproject/templates/auth/registration.html new file mode 100644 index 0000000..fb634eb --- /dev/null +++ b/myproject/myproject/templates/auth/registration.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block content %} + +{% if registration_errors %} +
+ +
+{% endif %} + +

Registration

+
+{% csrf_token %} + + + + + + + + + + + + + + + + + + + + + + + +
User*
Pass*
Pass (again)*
Email*
+ {{captcha_test|safe}} +
+ +
Fields with the asterick '*' are required
+
+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/base.html b/myproject/myproject/templates/base.html new file mode 100644 index 0000000..431dafb --- /dev/null +++ b/myproject/myproject/templates/base.html @@ -0,0 +1,21 @@ + + + + {{title}} + + + + +
+ {{user_navigation|safe}} +
+ +
+ {% block content %} + {% endblock %} +
+ + \ No newline at end of file diff --git a/myproject/myproject/templates/error.html b/myproject/myproject/templates/error.html new file mode 100644 index 0000000..03de67f --- /dev/null +++ b/myproject/myproject/templates/error.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content %} +
{{error}}
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/templates/index.html b/myproject/myproject/templates/index.html new file mode 100644 index 0000000..cc329a1 --- /dev/null +++ b/myproject/myproject/templates/index.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +

Index Page, no content yet.

+{% endblock %} \ No newline at end of file diff --git a/myproject/myproject/urls.py b/myproject/myproject/urls.py new file mode 100644 index 0000000..c8fb806 --- /dev/null +++ b/myproject/myproject/urls.py @@ -0,0 +1,31 @@ +''' + +Copyright (c) 2012, Kris Lamoureux +All rights reserved. + +DJ-BaseSite is released under the New BSD Liscense. +Please take a momemt to read the short 3 Clause LICENSE file. + +''' + + +from django.conf.urls import patterns, include, url +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('<%myproject%>.views', + + # Uncomment the next line to enable the admin: + url(r'^admin/', include(admin.site.urls)), + url(r'^$', 'index'), + url(r'^login/$', 'login_user'), + url(r'^logout/$', 'logout_user'), + url(r'^register/$', 'register_user'), + url(r'^activate/$', 'activate_user'), + #url(r'^deactivate/$', 'activate_user'), Deactivate needs to be written. +) + +urlpatterns += staticfiles_urlpatterns() \ No newline at end of file diff --git a/myproject/myproject/validation.py b/myproject/myproject/validation.py new file mode 100644 index 0000000..d853d26 --- /dev/null +++ b/myproject/myproject/validation.py @@ -0,0 +1,72 @@ +from re import match +from django.contrib.auth.models import User + +''' + Return Meanings + None (no data to check) + -1 (data too long/short) + -2 (user exists) + False (no match) + data (matched, data is valid) +''' + +def clean_username(data,length=30): + if data == "": + return None + if len(data) <= length: + if match("^[\w\d_]+$", data): + try: + user = User.objects.get(username__iexact=data) + return -2 + except User.DoesNotExist: + return data + else: + return False + else: + return -1 + +def clean_email(data,length=254): + if data == "": + return None + if len(data) <= length: + if match("^[\w\d._%-+]+@[\w\d._%-]+.[\w]{2,6}$", data): + try: + user = User.objects.get(email=data) + return -2 + except User.DoesNotExist: + return data + else: + return False + else: + return -1 + + +def clean_password(data,length_min=5): + if data == "": + return None + if len(data) >= length_min: + return data + else: + return -1 + + +def clean_key(data, length=25): + if data == "": + return None + if len(data) == 25: + if match("^[a-zABCDEFG0-9]+$", data): + return data + else: + return False + else: + return -1 + + +def clean_usernameRE(data): + if match("^[\w\d_]+$", data): + return data + else: + return False + + + \ No newline at end of file diff --git a/myproject/myproject/views.py b/myproject/myproject/views.py new file mode 100644 index 0000000..06ddfcc --- /dev/null +++ b/myproject/myproject/views.py @@ -0,0 +1,359 @@ +''' + +Copyright (c) 2012, Kris Lamoureux +All rights reserved. + +DJ-BaseSite is released under the New BSD Liscense. +Please take a momemt to read the short 3 Clause LICENSE file. + +''' + + +# Built in imports +import random +import hashlib + +# Responce imports +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response, RequestContext + +# Authentication/Session/Validation imports +from django.contrib.auth import authenticate, login, logout +from django.contrib.sessions.models import Session +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +# Below expected to be in same directory +import validation as v +import captcha + +# Email imports +from django.core.mail import EmailMessage +from django.core import mail + +# Variables from Settings.py +from settings import EMAIL_HOST_USER, baseurl, EMAIL_MESSAGE + +# User Profile model +from accountprofile.models import UserProfile + +# Website base title +base_title = "Base Site - " + +# Global Site Navigation +def global_nav(): + return 'Home' + +# User Site Navigation +def user_nav(user): + if not user: + return 'Login | Register' + else: + return ''+user+' | Logout' + +def get_or_create_profile(user): + try: + profile = user.get_profile() + except ObjectDoesNotExist: + profile = UserProfile(activated=True, user=user) + profile.save() + return profile + +def get_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + +def UserActivationKey(): + random.seed() + choices = "abcdefghijklmnopqrstuvwxyzABCDEFG0123456789" + + word = "" + for i in range(25): + x = random.randrange(0,len(choices)) + word = word + choices[x] + return word + + +''' PAGE REQUESTS ''' +def index(request): + global base_title + global global_nav, user_nav + + # Load Global Navigation and title. + title = base_title + "Home" + global_navigation=global_nav() + + # Load user navigation based on the session + if request.user.is_authenticated(): + user_navigation = user_nav(request.user.username) + else: + user_navigation = user_nav(False) + + responce = render_to_response('index.html', locals()) + return responce + +def logout_user(request): + logout(request) + return render_to_response('auth/logged_out.html', locals()) + +def login_user(request): + global base_title + global global_nav, user_nav + + title = base_title + "Login" + global_navigation=global_nav() + + # If user is not logged on + if not request.user.is_authenticated(): + + # Return user navigation for an anonymous session + user_navigation = user_nav(False) + + # If user has sent POST data + if request.method == 'POST': + + # Collect user data and return user object + usern = request.POST['username'] + passw = request.POST['password'] + + ''' CLEAN ''' + + user = authenticate(username=usern, password=passw) + + if user is not None: + # If user object exists it means the user is authenticated + # but you still need to check if the user.is_active + + if user.is_active: + # Account is active and not disabled. + # ... but is it activated? + user_profile = get_or_create_profile(user) + if user_profile.activated: + # User account is activated (via email) + login(request, user) + user_name = user.username + responce = render_to_response('auth/logged_in.html', locals()) + else: + # The account is not activated via email + error = "Please activate your account through email." + responce = render_to_response('error.html', locals()) + else: + # The account is disabled. No login. + message = "Your account has been disabled." + responce = render_to_response('auth/disabled.html', locals()) + + else: + # No object so the username and password are invalid. + login_errors = True + responce = render_to_response( + 'auth/login.html', + locals(), + context_instance=RequestContext(request) + ) + + else: + # User isn't online and hasn't sent any POST data, give them a login form. + responce = render_to_response( + 'auth/login.html', + locals(), + context_instance=RequestContext(request) + ) + + else: + # User is logged on, don't let them login until he's logged out. + user_navigation = user_nav(request.user.username) + error = "You're already logged on." + responce = render_to_response( + 'error.html', + locals() + ) + return responce + +def register_user(request): + global base_title + global global_nav, user_nav + + title = base_title + "Register" + global_navigation=global_nav() + + + # If user is not logged on + if not request.user.is_authenticated(): + + # Return user navigation for an anonymous session + user_navigation = user_nav(False) + + # Set up captcha html. + from settings import captcha_publickey, captcha_privatekey + captcha_test = captcha.displayhtml(captcha_publickey) + + # If user has sent POST data (not logged in) + if request.method == 'POST': + registration_errors = [] # Error list + + ''' Check and validate data ''' + + # Is human? + HumanTestResult = captcha.submit( + request.POST["recaptcha_challenge_field"], + request.POST["recaptcha_response_field"], + captcha_privatekey, + get_ip(request) + ) + + # If not human: display errors + if HumanTestResult.is_valid: + # Matching passwords? + password = v.clean_password(request.POST["passw"]) + if not request.POST["passw"] == request.POST["repassw"]: + registration_errors.append("Passwords don't match.") + if password == None: + registration_errors.append("No password entered.") + elif password == -1: + registration_errors.append("Passwords have to be at least 5 characters.") + + # Username related errors + username = v.clean_username(request.POST["usern"]) + if username == None: + registration_errors.append("No username entered.") + elif username == -2: + registration_errors.append("This username isn't available.") + elif username == -1: + registration_errors.append("Username's can only be 30 characters.") + elif username == False: + registration_errors.append("Username wasn't just characters numbers ") + + # Email related errors + email = v.clean_email(request.POST["email"]) + if email == None: + registration_errors.append("No email entered.") + elif email == -2: + registration_errors.append("This email already has an account.") + elif email == -1: + registration_errors.append("Emails can only be 245 characters.") + elif email == False: + registration_errors.append("Invalid email.") + + # Invalid CAPTCHA, display only that error giving no more information to the bot + else: + registration_errors.append("Invalid human verification code.") + captcha_test = captcha.displayhtml( + captcha_publickey, + False, + HumanTestResult.error_code) + + # If no errors: create user. + if len(registration_errors) == 0: + new_user = User.objects.create_user( + username, + email, + request.POST["repassw"] + ) + new_user.is_active = True + new_user.save() + + # Create activation key and user profile + activation_key = UserActivationKey() + profile = UserProfile( + activatekey=activation_key, + activated=False, + user=new_user) + profile.save() + + # User is created and saved. Send an activation link via email + connection = mail.get_connection() + connection.open() + + message_activateurl = baseurl+"/activate/?key="+str(activation_key)+"&user="+str(new_user.username) + message_deactivateurl = baseurl+"/deactivate/?key="+str(activation_key)+"&user="+str(new_user.username) + + f = open(EMAIL_MESSAGE, 'r') + message = f.read() + + message = message.replace("<$user>", str(new_user.username)) + message = message.replace("<$activatelink>", message_activateurl) + message = message.replace("<$disablelink>", message_deactivateurl) + + email = EmailMessage( + "Account Activation", + message, + EMAIL_HOST_USER, + [new_user.email]) + email.send() + connection.close() + + # Return new account page + accountname = new_user.username + responce = render_to_response( + 'auth/newaccount.html', + locals(), + context_instance=RequestContext(request) + ) + else: + # Return registration form with errors in registration_errors + responce = render_to_response( + 'auth/registration.html', + locals(), + context_instance=RequestContext(request) + ) + + # If user hasn't sent POST data (not logged on) + else: + responce = render_to_response( + 'auth/registration.html', + locals(), + context_instance=RequestContext(request) + ) + # User is logged on + else: + user_navigation = user_nav(request.user.username) + error = "You cannot register while logged in." + responce = render_to_response( + 'error.html', + locals() + ) + return responce + +def activate_user(request): + if request.method == 'GET': + # Check if data could be valid through regex + key = v.clean_key(request.GET["key"]) + u_name = v.clean_usernameRE(request.GET["user"]) + + # If key and username are valid + if request.GET["key"] == key and u_name: + try: + # Check profile for key and compare. + user = User.objects.get(username=u_name) + user_profile = get_or_create_profile(user) + + if user_profile.activatekey == key: + # Activate user + user_profile.activated = True + user_profile.save() + key_correct = True + else: + key_correct = False + + except ObjectDoesNotExist: + key_correct = False + else: + key_correct = False + + if key_correct: + user_name = user.username + responce = render_to_response( + 'auth/activated.html', + locals() + ) + else: + error = "Activation failed." + responce = render_to_response( + 'error.html', + locals() + ) + + return responce diff --git a/myproject/myproject/wsgi.py b/myproject/myproject/wsgi.py new file mode 100644 index 0000000..9790b66 --- /dev/null +++ b/myproject/myproject/wsgi.py @@ -0,0 +1,28 @@ +""" +WSGI config for <%myproject%> project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "<%myproject%>.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application)