This commit is contained in:
Samuel Clay 2009-06-16 03:08:55 +00:00
parent 52ccda2b6d
commit 48ba40e29b
1668 changed files with 46719 additions and 0 deletions

1
README Normal file
View file

@ -0,0 +1 @@
A News/RSS Reader that controls the amount, relevancy, and interestingness of news subscriptions.

0
__init__.py Normal file
View file

0
apps/__init__.py Normal file
View file

View file

6
apps/analyzer/models.py Normal file
View file

@ -0,0 +1,6 @@
from django.db import models
from django.contrib.auth.models import User
import datetime
from apps.rss_feeds.models import Feed, Story
from apps.reader.models import UserSubscription, ReadStories
from utils import feedparser, object_manager

1
apps/analyzer/views.py Normal file
View file

@ -0,0 +1 @@
# Create your views here.

View file

View file

@ -0,0 +1,5 @@
from django.db import models
from django.contrib.auth.models import User
from apps.rss_feeds.models import Feed, Story
import datetime

6
apps/opml_import/urls.py Normal file
View file

@ -0,0 +1,6 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('apps.opml_import.views',
(r'^$', 'opml_import'),
(r'^process', 'process'),
)

44
apps/opml_import/views.py Normal file
View file

@ -0,0 +1,44 @@
from django.shortcuts import render_to_response, get_list_or_404, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.template import RequestContext
from apps.rss_feeds.models import Feed, Story
from apps.reader.models import UserSubscription, ReadStories, UserSubscriptionFolders
from utils.json import json_encode
import utils.opml as opml
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpRequest
from django.core import serializers
from pprint import pprint
import datetime
def opml_import(request):
context = None
return render_to_response('opml_import/import.xhtml', context,
context_instance=RequestContext(request))
def process(request):
context = None
outline = opml.from_string(request.POST['opml'])
feeds = []
for folder in outline:
for feed in folder:
feed_data = dict(feed_address=feed.xmlUrl, feed_link=feed.htmlUrl, feed_title=feed.title)
feeds.append(feed_data)
new_feed = Feed(**feed_data)
try:
new_feed.save()
except:
new_feed = Feed.objects.get(**feed_data)
us = UserSubscription(feed=new_feed, user=request.user)
try:
us.save()
except:
us = UserSubscription.objects.get(feed=new_feed, user=request.user)
user_sub_folder = UserSubscriptionFolders(user=request.user, feed=new_feed, user_sub=us, folder=folder.text)
try:
user_sub_folder.save()
except:
pass
data = json_encode(feeds)
return HttpResponse(data, mimetype='application/json')

0
apps/profile/__init__.py Normal file
View file

5
apps/profile/models.py Normal file
View file

@ -0,0 +1,5 @@
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)

1
apps/profile/views.py Normal file
View file

@ -0,0 +1 @@
# Create your views here.

0
apps/reader/__init__.py Normal file
View file

6
apps/reader/admin.py Normal file
View file

@ -0,0 +1,6 @@
from apps.reader.models import UserSubscription, ReadStories, UserSubscriptionFolders
from django.contrib import admin
admin.site.register(UserSubscription)
admin.site.register(ReadStories)
admin.site.register(UserSubscriptionFolders)

113
apps/reader/models.py Normal file
View file

@ -0,0 +1,113 @@
from django.db import models
from django.contrib.auth.models import User
import datetime
from apps.rss_feeds.models import Feed, Story
from utils import feedparser, object_manager
class UserSubscription(models.Model):
user = models.ForeignKey(User)
feed = models.ForeignKey(Feed)
last_read_date = models.DateTimeField(default=datetime.datetime(2000,1,1))
mark_read_date = models.DateTimeField(default=datetime.datetime(2000,1,1))
unread_count = models.IntegerField(default=0)
unread_count_updated = models.DateTimeField(
default=datetime.datetime(2000,1,1)
)
def __unicode__(self):
return self.user.username + ': [' + self.feed.feed_title + '] '
def save(self, force_insert=False, force_update=False):
self.unread_count_updated = datetime.datetime.now()
super(UserSubscription, self).save(force_insert, force_update)
def get_user_feeds(self):
return Feed.objects.get(user=self.user, feed=feeds)
def count_unread(self):
if self.unread_count_updated > self.feed.last_update:
return self.unread_count
count = (self.stories_newer_lastread()
+ self.stories_between_lastread_allread())
if count == 0:
self.mark_read_date = datetime.datetime.now()
self.last_read_date = datetime.datetime.now()
self.unread_count_updated = datetime.datetime.now()
self.unread_count = 0
self.save()
else:
self.unread_count = count
self.unread_count_updated = datetime.datetime.now()
self.save()
return count
def mark_read(self):
self.last_read_date = datetime.datetime.now()
self.unread_count -= 1
self.unread_count_updated = datetime.datetime.now()
self.save()
def mark_feed_read(self):
self.last_read_date = datetime.datetime.now()
self.mark_read_date = datetime.datetime.now()
self.unread_count = 0
self.unread_count_updated = datetime.datetime.now()
self.save()
readstories = ReadStories.objects.filter(user=self.user, feed=self.feed)
readstories.delete()
def stories_newer_lastread(self):
return self.feed.new_stories_since_date(self.last_read_date)
def stories_between_lastread_allread(self):
story_count = Story.objects.filter(
story_date__gte=self.mark_read_date,
story_date__lte=self.last_read_date,
story_feed=self.feed
).count()
read_count = ReadStories.objects.filter(
feed=self.feed,
read_date__gte=self.mark_read_date,
read_date__lte=self.last_read_date
).count()
return story_count - read_count
def subscribe_to_feed(self, feed_id):
feed = Feed.objects.get(id=feed_id)
new_subscription = UserSubscription(user=self.user, feed=feed)
new_subscription.save()
class Meta:
unique_together = ("user", "feed")
class ReadStories(models.Model):
user = models.ForeignKey(User)
feed = models.ForeignKey(Feed)
story = models.ForeignKey(Story)
read_date = models.DateTimeField(auto_now=True)
def __unicode__(self):
return (self.user.username + ': [' + self.feed.feed_title + '] '
+ self.story.story_title)
class Meta:
verbose_name_plural = "read stories"
verbose_name = "read story"
unique_together = ("user", "story")
class UserSubscriptionFolders(models.Model):
user = models.ForeignKey(User)
user_sub = models.ForeignKey(UserSubscription)
feed = models.ForeignKey(Feed)
folder = models.CharField(max_length=255)
def __unicode__(self):
return (self.user.username + ': [' + self.feed.feed_title + '] '
+ self.folder)
class Meta:
verbose_name_plural = "folders"
verbose_name = "folder"
unique_together = ("user", "user_sub")

12
apps/reader/urls.py Normal file
View file

@ -0,0 +1,12 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('apps.reader.views',
(r'^$', 'index'),
(r'^load_single_feed', 'load_single_feed'),
(r'^load_feeds', 'load_feeds'),
(r'^refresh_all_feeds', 'refresh_all_feeds'),
(r'^refresh_feed', 'refresh_feed'),
(r'^mark_story_as_read', 'mark_story_as_read'),
(r'^mark_feed_as_read', 'mark_feed_as_read'),
(r'^get_read_feed_items', 'get_read_feed_items'),
)

200
apps/reader/views.py Normal file
View file

@ -0,0 +1,200 @@
from django.shortcuts import render_to_response, get_list_or_404, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.template import RequestContext
from apps.rss_feeds.models import Feed, Story
from apps.reader.models import UserSubscription, ReadStories, UserSubscriptionFolders
from utils.json import json_encode
from utils.story_functions import format_story_link_date__short, format_story_link_date__long
from utils.user_functions import get_user
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpRequest
from django.core import serializers
from pprint import pprint
from django.utils.safestring import mark_safe
from utils.feedcache.threading_model import fetch_feeds
import datetime
import threading
def index(request):
# feeds = Feed.objects.filter(usersubscription__user=request.user)
# for f in feeds:
# f.update()
# context = feeds
context = {}
user = request.user
user_info = _parse_user_info(user)
context.update(user_info)
return render_to_response('reader/feeds.xhtml', context,
context_instance=RequestContext(request))
def refresh_all_feeds(request):
force_update = False # request.GET.get('force', False)
feeds = Feed.objects.all()
t = threading.Thread(target=refresh_feeds,
args=[feeds])
t.setDaemon(True)
t.start()
# feeds = fetch_feeds(force_update, feeds)
context = {}
user = request.user
user_info = _parse_user_info(user)
context.update(user_info)
return render_to_response('reader/feeds.xhtml', context,
context_instance=RequestContext(request))
def refresh_feed(request):
feed_id = request.REQUEST['feed_id']
force_update = request.GET.get('force', False)
feeds = Feed.objects.filter(id=feed_id)
feeds = fetch_feeds(force_update, feeds)
context = {}
user = request.user
user_info = _parse_user_info(user)
context.update(user_info)
return render_to_response('reader/feeds.xhtml', context,
context_instance=RequestContext(request))
def refresh_feeds(feeds):
for f in feeds:
f.update()
return
def load_feeds(request):
user = get_user(request)
us = UserSubscriptionFolders.objects.select_related().filter(
user=user
)
feeds = []
folders = []
for sub in us:
sub.feed.unread_count = sub.user_sub.count_unread()
if sub.folder not in folders:
folders.append(sub.folder)
feeds.append({'folder': sub.folder, 'feeds': []})
for folder in feeds:
if folder['folder'] == sub.folder:
folder['feeds'].append(sub.feed)
# Alphabetize folders, then feeds inside folders
feeds.sort(lambda x, y: cmp(x['folder'].lower(), y['folder'].lower()))
for feed in feeds:
feed['feeds'].sort(lambda x, y: cmp(x.feed_title.lower(), y.feed_title.lower()))
for f in feed['feeds']:
f.feed_address = mark_safe(f.feed_address)
context = feeds
data = json_encode(context)
return HttpResponse(data, mimetype='application/json')
def load_single_feed(request):
user = get_user(request)
offset = int(request.REQUEST.get('offset', 0))
limit = int(request.REQUEST.get('limit', 25))
feed_id = request.REQUEST['feed_id']
stories=Story.objects.filter(story_feed=feed_id)[offset:offset+limit]
feed = Feed.objects.get(id=feed_id)
force_update = request.GET.get('force', False)
if force_update:
fetch_feeds(force_update, [feed])
us = UserSubscription.objects.select_related("feed").filter(user=user)
for sub in us:
if sub.feed_id == feed.id:
print "Feed: " + feed.feed_title
user_readstories = ReadStories.objects.filter(
user=user,
feed=feed
)
for story in stories:
story.short_parsed_date = format_story_link_date__short(story.story_date)
story.long_parsed_date = format_story_link_date__long(story.story_date)
story.story_feed_title = feed.feed_title
story.story_feed_link = mark_safe(feed.feed_link)
story.story_permalink = mark_safe(story.story_permalink)
if story.story_date < sub.mark_read_date:
story.read_status = 1
elif story.story_date > sub.last_read_date:
story.read_status = 0
else:
if story.id in [u_rs.story_id for u_rs in user_readstories]:
print "READ: "
story.read_status = 1
else:
print "unread: "
story.read_status = 0
context = stories
data = json_encode(context)
return HttpResponse(data, mimetype='text/plain')
@login_required
def mark_story_as_read(request):
story_id = request.REQUEST['story_id']
story = Story.objects.select_related("story_feed").get(id=story_id)
read_story = ReadStories.objects.filter(story=story_id, user=request.user, feed=story.story_feed).count()
print read_story
if read_story:
data = json_encode(dict(code=1))
else:
us = UserSubscription.objects.get(
feed=story.story_feed,
user=request.user
)
us.mark_read()
print "Marked Read: " + str(story_id) + ' ' + str(story.id)
m = ReadStories(story=story, user=request.user, feed=story.story_feed)
data = json_encode(dict(code=0))
try:
m.save()
except:
data = json_encode(dict(code=2))
return HttpResponse(data)
@login_required
def mark_feed_as_read(request):
feed_id = int(request.REQUEST['feed_id'])
feed = Feed.objects.get(id=feed_id)
us = UserSubscription.objects.get(feed=feed, user=request.user)
us.mark_feed_read()
ReadStories.objects.filter(user=request.user, feed=feed_id).delete()
data = json_encode(dict(code=0))
try:
m.save()
except:
data = json_encode(dict(code=1))
return HttpResponse(data)
@login_required
def get_read_feed_items(request, username):
feeds = get_list_or_404(Feed)
def _parse_user_info(user):
return {
'user_info': {
'is_anonymous': json_encode(user.is_anonymous()),
'is_authenticated': json_encode(user.is_authenticated()),
'username': user.username if user.is_authenticated() else 'Anonymous'
}
}

View file

View file

@ -0,0 +1,11 @@
from django.contrib import admin
from apps.registration.models import RegistrationProfile
class RegistrationAdmin(admin.ModelAdmin):
list_display = ('__unicode__', 'activation_key_expired')
search_fields = ('user__username', 'user__first_name')
admin.site.register(RegistrationProfile, RegistrationAdmin)

View file

@ -0,0 +1,82 @@
=====
Forms
=====
To ease and automate the process of validating user information during
registration, several form classes (built using Django's `newforms
library`_) are provided: a base ``RegistrationForm`` and subclasses
which provide specific customized functionality. All of the forms
described below are found in ``registration.forms``.
.. _newforms library: http://www.djangoproject.com/documentation/newforms/
``RegistrationForm``
====================
Form for registering a new user account.
Validates that the requested username is not already in use, and
requires the password to be entered twice to catch typos.
Subclasses should feel free to add any additional validation they
need, but should either preserve the base ``save()`` or implement a
``save()`` which accepts the ``profile_callback`` keyword argument and
passes it through to
``RegistrationProfile.objects.create_inactive_user()``.
Fields:
``username``
The new user's requested username. Will be validated according to
the same regular expression Django's authentication system uses to
validate usernames.
``email``
The new user's email address. Must be a well-formed email address.
``password1``
The new user's password.
``password2``
The password, again, to catch typos.
Non-validation methods:
``save()``
Creates the new ``User`` and ``RegistrationProfile``, and returns
the ``User``.
This is essentially a light wrapper around
``RegistrationProfile.objects.create_inactive_user()``, feeding it
the form data and a profile callback (see the documentation on
``create_inactive_user()`` for details) if supplied.
Subclasses of ``RegistrationForm``
==================================
As explained above, subclasses may add any additional validation they
like, but must either preserve the ``save()`` method or implement a
``save()`` method with an identical signature.
Three subclasses are included as examples, and as ready-made
implementations of useful customizations:
``RegistrationFormTermsOfService``
Subclass of ``RegistrationForm`` which adds a required checkbox
for agreeing to a site's Terms of Service.
``RegistrationFormUniqueEmail``
Subclass of ``RegistrationForm`` which enforces uniqueness of
email addresses.
``RegistrationFormNoFreeEmail``
Subclass of ``RegistrationForm`` which disallows registration with
email addresses from popular free webmail services; moderately
useful for preventing automated spam registrations.
To change the list of banned domains, subclass this form and
override the attribute ``bad_domains``.

View file

@ -0,0 +1,174 @@
===================
Models and managers
===================
Because the two-step process of registration and activation requires
some means of temporarily storing activation key and retrieving it for
verification, a simple model --
``registration.models.RegistrationProfile`` -- is provided in this
application, and a custom manager --
``registration.models.RegistrationManager`` -- is included and defines
several useful methods for interacting with ``RegistrationProfile``.
Both the ``RegistrationProfile`` model and the ``RegistrationManager``
are found in ``registration.models``.
The ``RegistrationProfile`` model
=================================
A simple profile which stores an activation key for use during user
account registration.
Generally, you will not want to interact directly with instances of
this model; the provided manager includes methods for creating and
activating new accounts, as well as for cleaning out accounts which
have never been activated.
While it is possible to use this model as the value of the
``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do
so. This model's sole purpose is to store data temporarily during
account registration and activation, and a mechanism for automatically
creating an instance of a site-specific profile model is provided via
the ``create_inactive_user`` on ``RegistrationManager``.
``RegistrationProfile`` objects have the following fields:
``activation_key``
A SHA1 hash used as an account's activation key.
``user``
The ``User`` object for which activation information is being
stored.
``RegistrationProfile`` also has one custom method defined:
``activation_key_expired()``
Determines whether this ``RegistrationProfile``'s activation key
has expired.
Returns ``True`` if the key has expired, ``False`` otherwise.
Key expiration is determined by a two-step process:
1. If the user has already activated, the key will have been reset
to the string ``ALREADY_ACTIVATED``. Re-activating is not
permitted, and so this method returns ``True`` in this case.
2. Otherwise, the date the user signed up is incremented by the
number of days specified in the setting
``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of days
after signup during which a user is allowed to activate their
account); if the result is less than or equal to the current
date, the key has expired and this method returns ``True``.
The ``RegistrationManager``
===========================
Custom manager for the ``RegistrationProfile`` model.
The methods defined here provide shortcuts for account creation and
activation (including generation and emailing of activation keys), and
for cleaning out expired inactive accounts.
Methods:
``activate_user(activation_key)``
Validates an activation key and activates the corresponding
``User`` if valid.
If the key is valid and has not expired, returns the ``User``
after activating.
If the key is not valid or has expired, returns ``False``.
If the key is valid but the ``User`` is already active, returns
``False``.
To prevent reactivation of an account which has been deactivated
by site administrators, the activation key is reset to the string
``ALREADY_ACTIVATED`` after successful activation.
``create_inactive_user(username, password, email, send_email=True, profile_callback=None)``
Creates a new, inactive ``User``, generates a
``RegistrationProfile`` and emails its activation key to the
``User``. Returns the new ``User``.
To disable the email, call with ``send_email=False``.
The activation email will make use of two templates:
``registration/activation_email_subject.txt``
This template will be used for the subject line of the
email. It receives one context variable, ``site``, which is
the currently-active ``django.contrib.sites.models.Site``
instance. Because it is used as the subject line of an email,
this template's output **must** be only a single line of text;
output longer than one line will be forcibly joined into only
a single line.
``registration/activation_email.txt``
This template will be used for the body of the email. It will
receive three context variables: ``activation_key`` will be
the user's activation key (for use in constructing a URL to
activate the account), ``expiration_days`` will be the number
of days for which the key will be valid and ``site`` will be
the currently-active ``django.contrib.sites.models.Site``
instance.
To enable creation of a custom user profile along with the
``User`` (e.g., the model specified in the ``AUTH_PROFILE_MODULE``
setting), define a function which knows how to create and save an
instance of that model with appropriate default values, and pass
it as the keyword argument ``profile_callback``. This function
should accept one keyword argument:
``user``
The ``User`` to relate the profile to.
``create_profile(user)``
Creates a ``RegistrationProfile`` for a given ``User``. Returns
the ``RegistrationProfile``.
The activation key for the ``RegistrationProfile`` will be a SHA1
hash, generated from a combination of the ``User``'s username and
a random salt.
``deleted_expired_users()``
Removes expired instances of ``RegistrationProfile`` and their
associated ``User`` objects.
Accounts to be deleted are identified by searching for instances
of ``RegistrationProfile`` with expired activation keys, and then
checking to see if their associated ``User`` instances have the
field ``is_active`` set to ``False``; any ``User`` who is both
inactive and has an expired activation key will be deleted.
It is recommended that this method be executed regularly as part
of your routine site maintenance; this application provides a
custom management command which will call this method, accessible
as ``manage.py cleanupregistration``.
Regularly clearing out accounts which have never been activated
serves two useful purposes:
1. It alleviates the ocasional need to reset a
``RegistrationProfile`` and/or re-send an activation email when
a user does not receive or does not act upon the initial
activation email; since the account will be deleted, the user
will be able to simply re-register and receive a new activation
key.
2. It prevents the possibility of a malicious user registering one
or more accounts and never activating them (thus denying the
use of those usernames to anyone else); since those accounts
will be deleted, the usernames will become available for use
again.
If you have a troublesome ``User`` and wish to disable their
account while keeping it in the database, simply delete the
associated ``RegistrationProfile``; an inactive ``User`` which
does not have an associated ``RegistrationProfile`` will not be
deleted.

View file

@ -0,0 +1,337 @@
===================
Django registration
===================
This is a fairly simple user-registration application for Django_,
designed to make allowing user signups as painless as possible.
.. _Django: http://www.djangoproject.com/
Overview
========
This application enables a common user-registration workflow:
1. User fills out a registration form, selecting a username and
password and entering an email address.
2. An inactive account is created, and an activation link is sent to
the user's email address.
3. User clicks the activation link, the account becomes active and the
user is able to log in and begin contributing to your site.
Various methods of extending and customizing the registration process
are also provided.
Installation
============
In order to use django-registration, you will need to have a
functioning installation of Django 1.0 or newer; due to changes needed
to stabilize Django's APIs prior to the 1.0 release,
django-registration will not work with older releases of Django.
There are three basic ways to install django-registration:
automatically installing a package using Python's package-management
tools, manually installing a package, and installing from a Mercurial
Using a package-management tool
-------------------------------
The easiest way by far to install django-registration and most other
interesting Python software is by using an automated
package-management tool, so if you're not already familiar with the
available tools for Python, now's as good a time as any to get
started.
The most popular option currently is `easy_install`_; refer to its
documentation to see how to get it set up. Once you've got it, you'll
be able to simply type::
easy_install django-registration
And it will handle the rest.
Another option that's currently gaining steam (and which I personally
prefer for Python package management) is `pip`_. Once again, you'll
want to refer to its documentation to get up and running, but once you
have you'll be able to type::
pip install django-registration
And you'll be done.
Manually installing the 0.7 package
-----------------------------------
If you'd prefer to do things the old-fashioned way, you can manually
download the `django-registration 0.7 package`_ from the Python
Package Index. This will get you a file named
"django-registration-0.7.tar.gz" which you can unpack (double-click on
the file on most operating systems) to create a directory named
"django-registration-0.7". Inside will be a script named "setup.py";
running::
python setup.py install
will install django-registration (though keep in mind that this
defaults to a system-wide installation, and so may require
administrative privileges on your computer).
Installing from a Mercurial checkout
------------------------------------
If you have `Mercurial`_ installed on your computer, you can also
obtain a complete copy of django-registration by typing::
hg clone http://bitbucket.org/ubernostrum/django-registration/
Inside the resulting "django-registration" directory will be a
directory named "registration", which is the actual Python module for
this application; you can symlink it from somewhere on your Python
path. If you prefer, you can use the setup.py script in the
"django-registration" directory to perform a normal installation, but
using a symlink offers easy upgrades: simply running ``hg pull -u``
inside the django-registration directory will fetch updates from the
main repository and apply them to your local copy.
.. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall
.. _pip: http://pypi.python.org/pypi/pip/
.. _django-registration 0.7 package: http://pypi.python.org/pypi/django-registration/0.7
.. _Mercurial: http://www.selenic.com/mercurial/wiki/
Basic use
=========
To use the registration system with all its default settings, you'll
need to do the following:
1. Add ``registration`` to the ``INSTALLED_APPS`` setting of your
Django project.
2. Add the setting ``ACCOUNT_ACTIVATION_DAYS`` to your settings file;
this should be the number of days activation keys will remain valid
after an account is registered.
3. Create the necessary templates (see the section on templates below
for details).
4. Add this line to your site's root URLConf::
(r'^accounts/', include('registration.urls')),
5. Link people to ``/accounts/register/`` so they can start signing
up. Using the default URLConf will also automatically set up the
authentication-oriented views in ``django.contrib.auth`` for you,
so if you use it you can point people to, e.g.,
``/accounts/login/`` to log in.
Templates used by django-registration
=====================================
The views included in django-registration make use of five templates:
* ``registration/registration_form.html`` displays the registration
form for users to sign up.
* ``registration/registration_complete.html`` is displayed after the
activation email has been sent, to tell the new user to check
his/her email.
* ``registration/activation_email_subject.txt`` is used for the
subject of the activation email.
* ``registration/activation_email.txt`` is used for the body of the
activation email.
* ``registration/activate.html`` is displayed when a user attempts to
activate his/her account.
Examples of all of these templates are not provided; you will need to
create them yourself. For views defined in this application, see the
included `views documentation`_ for details on available context
variables, and for details on the templates used by the activation
email see the included `models documentation`_.
Additionally, the URLConf provided with django-registration includes
URL patterns for useful views in Django's built-in authentication
application -- this means that a single ``include`` in your root
URLConf can wire up registration and the auth application's login,
logout, and password change/reset views. If you choose to use these
views you will need to provide your own templates for them; consult
`the Django authentication documentation`_ for details on the
templates and contexts used by these views.
.. _views documentation: views.txt
.. _models documentation: models.txt
.. _the Django authentication documentation: http://www.djangoproject.com/documentation/authentication/
How it works
============
Using the recommended default configuration, the URL
``/accounts/register/`` will map to the view
``registration.views.register``, which displays a registration form
(an instance of ``registration.forms.RegistrationForm``); this form
asks for a username, email address and password, and verifies that the
username is available and requires the password to be entered twice
(to catch typos). It then does three things:
1. Creates an instance of ``django.contrib.models.auth.User``, using
the supplied username, email address and password; the
``is_active`` field on the new ``User`` will be set to ``False``,
meaning that the account is inactive and the user will not be able
to log in yet.
2. Creates an instance of ``registration.models.RegistrationProfile``,
stores an activation key (a SHA1 hash generated from the new user's
username plus a randomly-generated "salt"), and relates that
``RegistrationProfile`` to the ``User`` it just created.
3. Sends an email to the user (at the address they supplied)
containing a link which can be clicked to activate the account.
For details on customizing this process, including use of alternate
registration form classes and automatic creation of a site-specific
profile, see the sections on customization below.
After the activation email has been sent,
``registration.views.register`` issues a redirect to the URL
``/accounts/register/complete/``. By default, this is mapped to the
``direct_to_template`` generic view, and displays the template
``registration/registration_complete.html``; this is intended to show
a short message telling the user to check his/her email for the
activation link.
The activation link will map to the view
``registration.views.activate``, which will attempt to activate the
account by setting the ``is_active`` field on the ``User`` to
``True``. If the activation key for the ``User`` has expired (this is
controlled by the setting ``ACCOUNT_ACTIVATION_DAYS``, as described
above), the account will not be activated (see the section on
maintenance below for instructions on cleaning out expired accounts
which have not been activated).
Maintenance
===========
Inevitably, a site which uses a two-step process for user signup --
registration followed by activation -- will accumulate a certain
number of accounts which were registered but never activated. These
accounts clutter up the database and tie up usernames which might
otherwise be actively used, so it's desirable to clean them out
periodically. For this purpose, a script,
``registration/bin/deleted_expired_users.py``, is provided, which is
suitable for use as a regular cron job. See that file for notes on how
to add it to your crontab, and the included models documentation (see
below) for discussion of how it works and some caveats.
Where to go from here
=====================
Full documentation for all included components is bundled in the
packaged release; see the following files for details:
* `Forms documentation`_ for details on ``RegistrationForm``,
pre-packaged subclasses and available customizations.
* `Models documentation`_ for details on ``RegistrationProfile`` and
its custom manager.
* `Views documentation`_ for details on the ``register`` and
``activate`` views, and methods for customizing them.
.. _Forms documentation: forms.txt
.. _Models documentation: models.txt
.. _Views documentation: views.txt
Development
===========
The `latest released version`_ of this application is 0.7, and is
quite stable; it's already been deployed on a number of sites,
including djangoproject.com. You can also obtain the absolute freshest
code from `the development repository_`, but be warned that the
development code may not always be backwards-compatible, and may well
contain bugs that haven't yet been fixed.
This document covers the 0.7 release of django-registration; new
features introduced in the development trunk will be added to the
documentation at the time of the next packaged release.
.. _latest released version: http://pypi.python.org/pypi/django-registration/0.7
.. _the development repository: http://www.bitbucket.org/ubernostrum/django-registration/src/
Changes from previous versions
==============================
Several new features were added between version 0.2 and version 0.3;
for details, see the CHANGELOG.txt file distributed with the packaged
0.3 release.
One important change to note before upgrading an installation of
version 0.1 is a change to the ``RegistrationProfile`` model; the
field ``key_generated`` has been removed, since it was redundant with
the field ``date_joined`` on Django's bundled ``User`` model. Since
this field became a ``NOT NULL`` column in the database, you will need
to either drop the ``NOT NULL`` constraint or, preferably, simply drop
the column. Consult your database's documentation for the correct way
to handle this.
Between version 0.3 and version 0.4, validation of the password fields
was moved from ``clean_password2()`` to ``clean_password()``; this
means that errors from mismatched passwords will now show up in
``non_field_errors()`` instead of ``errors["password2"]``.
Between version 0.6 and version 0.7, the script
``registration/bin/delete_expired_users.py`` was removed, and replaced
with a custom management command; you can now simply run ``manage.py
cleanupregistration`` from any project which has django-registration
installed.
Dependencies
============
The only dependencies for this application are a functioning install
of Django 1.0 or newer and, of course, a Django project in which you'd
like to use it.
Your Django project should have ``django.contrib.admin``,
``django.contrib.auth`` and ``django.contrib.sites`` in its
``INSTALLED_APPS`` setting.
What this application does not do
=================================
This application does not integrate in any way with OpenID, nor should
it; one of the key selling points of OpenID is that users **don't**
have to walk through an explicit registration step for every site or
service they want to use :)
If you spot a bug
=================
Head over to this application's `project page on Bitbucket`_ and
check `the issues list`_ to see if it's already been reported. If not,
open a new issue and I'll do my best to respond quickly.
.. _project page on Bitbucket: http://www.bitbucket.org/ubernostrum/django-registration/overview/
.. _the issues list: http://www.bitbucket.org/ubernostrum/django-registration/issues/

View file

@ -0,0 +1,120 @@
=====
Views
=====
Two views are included which, between them, handle the process of
first registering and then activating new user accounts; both views
are found in ``registration.views``.
``activate``
============
Activate a ``User``'s account from an activation key, if their key is
valid and hasn't expired.
By default, use the template ``registration/activate.html``; to
change this, pass the name of a template as the keyword argument
``template_name``.
**Required arguments**
``activation_key``
The activation key to validate and use for activating the
``User``.
**Optional arguments**
``extra_context``
A dictionary of variables to add to the template context. Any
callable object in this dictionary will be called to produce the
end result which appears in the context.
``template_name``
A custom template to use.
**Context:**
``account``
The ``User`` object corresponding to the account, if the
activation was successful. ``False`` if the activation was not
successful.
``expiration_days``
The number of days for which activation keys stay valid after
registration.
Any extra variables supplied in the ``extra_context`` argument (see
above).
**Template:**
registration/activate.html or ``template_name`` keyword argument.
``register``
============
Allow a new user to register an account.
Following successful registration, issue a redirect; by default, this
will be whatever URL corresponds to the named URL pattern
``registration_complete``, which will be
``/accounts/register/complete/`` if using the included URLConf. To
change this, point that named pattern at another URL, or pass your
preferred URL as the keyword argument ``success_url``.
By default, ``registration.forms.RegistrationForm`` will be used as
the registration form; to change this, pass a different form class as
the ``form_class`` keyword argument. The form class you specify must
have a method ``save`` which will create and return the new ``User``,
and that method must accept the keyword argument ``profile_callback``
(see below).
To enable creation of a site-specific user profile object for the new
user, pass a function which will create the profile object as the
keyword argument ``profile_callback``. See
``RegistrationManager.create_inactive_user`` in the file ``models.py``
for details on how to write this function.
By default, use the template ``registration/registration_form.html``;
to change this, pass the name of a template as the keyword argument
``template_name``.
**Required arguments**
None.
**Optional arguments**
``form_class``
The form class to use for registration.
``extra_context``
A dictionary of variables to add to the template context. Any
callable object in this dictionary will be called to produce the
end result which appears in the context.
``profile_callback``
A function which will be used to create a site-specific profile
instance for the new ``User``.
``success_url``
The URL to redirect to on successful registration.
``template_name``
A custom template to use.
**Context:**
``form``
The registration form.
Any extra variables supplied in the ``extra_context`` argument (see
above).
**Template:**
registration/registration_form.html or ``template_name`` keyword
argument.

142
apps/registration/forms.py Normal file
View file

@ -0,0 +1,142 @@
"""
Forms and validation code for user registration.
"""
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from apps.registration.models import RegistrationProfile
# I put this on all required fields, because it's easier to pick up
# on them with CSS or JavaScript if they have a class of "required"
# in the HTML. Your mileage may vary. If/when Django ticket #3515
# lands in trunk, this will no longer be necessary.
attrs_dict = { 'class': 'required' }
class RegistrationForm(forms.Form):
"""
Form for registering a new user account.
Validates that the requested username is not already in use, and
requires the password to be entered twice to catch typos.
Subclasses should feel free to add any additional validation they
need, but should either preserve the base ``save()`` or implement
a ``save()`` which accepts the ``profile_callback`` keyword
argument and passes it through to
``RegistrationProfile.objects.create_inactive_user()``.
"""
username = forms.RegexField(regex=r'^\w+$',
max_length=30,
widget=forms.TextInput(attrs=attrs_dict),
label=_(u'username'))
email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict,
maxlength=75)),
label=_(u'email address'))
password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False),
label=_(u'password'))
password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False),
label=_(u'password (again)'))
def clean_username(self):
"""
Validate that the username is alphanumeric and is not already
in use.
"""
try:
user = User.objects.get(username__iexact=self.cleaned_data['username'])
except User.DoesNotExist:
return self.cleaned_data['username']
raise forms.ValidationError(_(u'This username is already taken. Please choose another.'))
def clean(self):
"""
Verifiy that the values entered into the two password fields
match. Note that an error here will end up in
``non_field_errors()`` because it doesn't apply to a single
field.
"""
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise forms.ValidationError(_(u'You must type the same password each time'))
return self.cleaned_data
def save(self, profile_callback=None):
"""
Create the new ``User`` and ``RegistrationProfile``, and
returns the ``User``.
This is essentially a light wrapper around
``RegistrationProfile.objects.create_inactive_user()``,
feeding it the form data and a profile callback (see the
documentation on ``create_inactive_user()`` for details) if
supplied.
"""
new_user = RegistrationProfile.objects.create_inactive_user(username=self.cleaned_data['username'],
password=self.cleaned_data['password1'],
email=self.cleaned_data['email'],
profile_callback=profile_callback)
return new_user
class RegistrationFormTermsOfService(RegistrationForm):
"""
Subclass of ``RegistrationForm`` which adds a required checkbox
for agreeing to a site's Terms of Service.
"""
tos = forms.BooleanField(widget=forms.CheckboxInput(attrs=attrs_dict),
label=_(u'I have read and agree to the Terms of Service'),
error_messages={ 'required': u"You must agree to the terms to register" })
class RegistrationFormUniqueEmail(RegistrationForm):
"""
Subclass of ``RegistrationForm`` which enforces uniqueness of
email addresses.
"""
def clean_email(self):
"""
Validate that the supplied email address is unique for the
site.
"""
if User.objects.filter(email__iexact=self.cleaned_data['email']):
raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
return self.cleaned_data['email']
class RegistrationFormNoFreeEmail(RegistrationForm):
"""
Subclass of ``RegistrationForm`` which disallows registration with
email addresses from popular free webmail services; moderately
useful for preventing automated spam registrations.
To change the list of banned domains, subclass this form and
override the attribute ``bad_domains``.
"""
bad_domains = ['aim.com', 'aol.com', 'email.com', 'gmail.com',
'googlemail.com', 'hotmail.com', 'hushmail.com',
'msn.com', 'mail.ru', 'mailinator.com', 'live.com']
def clean_email(self):
"""
Check the supplied email address against a list of known free
webmail domains.
"""
email_domain = self.cleaned_data['email'].split('@')[1]
if email_domain in self.bad_domains:
raise forms.ValidationError(_(u'Registration using free email addresses is prohibited. Please supply a different email address.'))
return self.cleaned_data['email']

View file

View file

@ -0,0 +1,19 @@
"""
A management command which deletes expired accounts (e.g.,
accounts which signed up but never activated) from the database.
Calls ``RegistrationProfile.objects.delete_expired_users()``, which
contains the actual logic for determining which accounts are deleted.
"""
from django.core.management.base import NoArgsCommand
from apps.registration.models import RegistrationProfile
class Command(NoArgsCommand):
help = "Delete expired user registrations from the database"
def handle_noargs(self, **options):
RegistrationProfile.objects.delete_expired_users()

250
apps/registration/models.py Normal file
View file

@ -0,0 +1,250 @@
import datetime
import random
import re
import sha
from django.conf import settings
from django.db import models
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
SHA1_RE = re.compile('^[a-f0-9]{40}$')
class RegistrationManager(models.Manager):
"""
Custom manager for the ``RegistrationProfile`` model.
The methods defined here provide shortcuts for account creation
and activation (including generation and emailing of activation
keys), and for cleaning out expired inactive accounts.
"""
def activate_user(self, activation_key):
"""
Validate an activation key and activate the corresponding
``User`` if valid.
If the key is valid and has not expired, return the ``User``
after activating.
If the key is not valid or has expired, return ``False``.
If the key is valid but the ``User`` is already active,
return ``False``.
To prevent reactivation of an account which has been
deactivated by site administrators, the activation key is
reset to the string ``ALREADY_ACTIVATED`` after successful
activation.
"""
# Make sure the key we're trying conforms to the pattern of a
# SHA1 hash; if it doesn't, no point trying to look it up in
# the database.
if SHA1_RE.search(activation_key):
try:
profile = self.get(activation_key=activation_key)
except self.model.DoesNotExist:
return False
if not profile.activation_key_expired():
user = profile.user
user.is_active = True
user.save()
profile.activation_key = self.model.ACTIVATED
profile.save()
return user
return False
def create_inactive_user(self, username, password, email,
send_email=True, profile_callback=None):
"""
Create a new, inactive ``User``, generates a
``RegistrationProfile`` and email its activation key to the
``User``, returning the new ``User``.
To disable the email, call with ``send_email=False``.
The activation email will make use of two templates:
``registration/activation_email_subject.txt``
This template will be used for the subject line of the
email. It receives one context variable, ``site``, which
is the currently-active
``django.contrib.sites.models.Site`` instance. Because it
is used as the subject line of an email, this template's
output **must** be only a single line of text; output
longer than one line will be forcibly joined into only a
single line.
``registration/activation_email.txt``
This template will be used for the body of the email. It
will receive three context variables: ``activation_key``
will be the user's activation key (for use in constructing
a URL to activate the account), ``expiration_days`` will
be the number of days for which the key will be valid and
``site`` will be the currently-active
``django.contrib.sites.models.Site`` instance.
To enable creation of a custom user profile along with the
``User`` (e.g., the model specified in the
``AUTH_PROFILE_MODULE`` setting), define a function which
knows how to create and save an instance of that model with
appropriate default values, and pass it as the keyword
argument ``profile_callback``. This function should accept one
keyword argument:
``user``
The ``User`` to relate the profile to.
"""
new_user = User.objects.create_user(username, email, password)
new_user.is_active = False
new_user.save()
registration_profile = self.create_profile(new_user)
if profile_callback is not None:
profile_callback(user=new_user)
if send_email:
from django.core.mail import send_mail
current_site = Site.objects.get_current()
subject = render_to_string('registration/activation_email_subject.txt',
{ 'site': current_site })
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
message = render_to_string('registration/activation_email.txt',
{ 'activation_key': registration_profile.activation_key,
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
'site': current_site })
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [new_user.email])
return new_user
def create_profile(self, user):
"""
Create a ``RegistrationProfile`` for a given
``User``, and return the ``RegistrationProfile``.
The activation key for the ``RegistrationProfile`` will be a
SHA1 hash, generated from a combination of the ``User``'s
username and a random salt.
"""
salt = sha.new(str(random.random())).hexdigest()[:5]
activation_key = sha.new(salt+user.username).hexdigest()
return self.create(user=user,
activation_key=activation_key)
def delete_expired_users(self):
"""
Remove expired instances of ``RegistrationProfile`` and their
associated ``User``s.
Accounts to be deleted are identified by searching for
instances of ``RegistrationProfile`` with expired activation
keys, and then checking to see if their associated ``User``
instances have the field ``is_active`` set to ``False``; any
``User`` who is both inactive and has an expired activation
key will be deleted.
It is recommended that this method be executed regularly as
part of your routine site maintenance; this application
provides a custom management command which will call this
method, accessible as ``manage.py cleanupregistration``.
Regularly clearing out accounts which have never been
activated serves two useful purposes:
1. It alleviates the ocasional need to reset a
``RegistrationProfile`` and/or re-send an activation email
when a user does not receive or does not act upon the
initial activation email; since the account will be
deleted, the user will be able to simply re-register and
receive a new activation key.
2. It prevents the possibility of a malicious user registering
one or more accounts and never activating them (thus
denying the use of those usernames to anyone else); since
those accounts will be deleted, the usernames will become
available for use again.
If you have a troublesome ``User`` and wish to disable their
account while keeping it in the database, simply delete the
associated ``RegistrationProfile``; an inactive ``User`` which
does not have an associated ``RegistrationProfile`` will not
be deleted.
"""
for profile in self.all():
if profile.activation_key_expired():
user = profile.user
if not user.is_active:
user.delete()
class RegistrationProfile(models.Model):
"""
A simple profile which stores an activation key for use during
user account registration.
Generally, you will not want to interact directly with instances
of this model; the provided manager includes methods
for creating and activating new accounts, as well as for cleaning
out accounts which have never been activated.
While it is possible to use this model as the value of the
``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do
so. This model's sole purpose is to store data temporarily during
account registration and activation, and a mechanism for
automatically creating an instance of a site-specific profile
model is provided via the ``create_inactive_user`` on
``RegistrationManager``.
"""
ACTIVATED = u"ALREADY_ACTIVATED"
user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
activation_key = models.CharField(_('activation key'), max_length=40)
objects = RegistrationManager()
class Meta:
verbose_name = _('registration profile')
verbose_name_plural = _('registration profiles')
def __unicode__(self):
return u"Registration information for %s" % self.user
def activation_key_expired(self):
"""
Determine whether this ``RegistrationProfile``'s activation
key has expired, returning a boolean -- ``True`` if the key
has expired.
Key expiration is determined by a two-step process:
1. If the user has already activated, the key will have been
reset to the string ``ALREADY_ACTIVATED``. Re-activating is
not permitted, and so this method returns ``True`` in this
case.
2. Otherwise, the date the user signed up is incremented by
the number of days specified in the setting
``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of
days after signup during which a user is allowed to
activate their account); if the result is less than or
equal to the current date, the key has expired and this
method returns ``True``.
"""
expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
return self.activation_key == self.ACTIVATED or \
(self.user.date_joined + expiration_date <= datetime.datetime.now())
activation_key_expired.boolean = True

318
apps/registration/tests.py Normal file
View file

@ -0,0 +1,318 @@
"""
Unit tests for django-registration.
These tests assume that you've completed all the prerequisites for
getting django-registration running in the default setup, to wit:
1. You have ``registration`` in your ``INSTALLED_APPS`` setting.
2. You have created all of the templates mentioned in this
application's documentation.
3. You have added the setting ``ACCOUNT_ACTIVATION_DAYS`` to your
settings file.
4. You have URL patterns pointing to the registration and activation
views, with the names ``registration_register`` and
``registration_activate``, respectively, and a URL pattern named
'registration_complete'.
"""
import datetime
import sha
from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail
from django.core import management
from django.core.urlresolvers import reverse
from django.test import TestCase
from apps.registration import forms
from apps.registration.models import RegistrationProfile
class RegistrationTestCase(TestCase):
"""
Base class for the test cases; this sets up two users -- one
expired, one not -- which are used to exercise various parts of
the application.
"""
def setUp(self):
self.sample_user = RegistrationProfile.objects.create_inactive_user(username='alice',
password='secret',
email='alice@example.com')
self.expired_user = RegistrationProfile.objects.create_inactive_user(username='bob',
password='swordfish',
email='bob@example.com')
self.expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1)
self.expired_user.save()
class RegistrationModelTests(RegistrationTestCase):
"""
Tests for the model-oriented functionality of django-registration,
including ``RegistrationProfile`` and its custom manager.
"""
def test_new_user_is_inactive(self):
"""
Test that a newly-created user is inactive.
"""
self.failIf(self.sample_user.is_active)
def test_registration_profile_created(self):
"""
Test that a ``RegistrationProfile`` is created for a new user.
"""
self.assertEqual(RegistrationProfile.objects.count(), 2)
def test_activation_email(self):
"""
Test that user signup sends an activation email.
"""
self.assertEqual(len(mail.outbox), 2)
def test_activation(self):
"""
Test that user activation actually activates the user and
properly resets the activation key, and fails for an
already-active or expired user, or an invalid key.
"""
# Activating a valid user returns the user.
self.failUnlessEqual(RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.sample_user).activation_key).pk,
self.sample_user.pk)
# The activated user must now be active.
self.failUnless(User.objects.get(pk=self.sample_user.pk).is_active)
# The activation key must now be reset to the "already activated" constant.
self.failUnlessEqual(RegistrationProfile.objects.get(user=self.sample_user).activation_key,
RegistrationProfile.ACTIVATED)
# Activating an expired user returns False.
self.failIf(RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.expired_user).activation_key))
# Activating from a key that isn't a SHA1 hash returns False.
self.failIf(RegistrationProfile.objects.activate_user('foo'))
# Activating from a key that doesn't exist returns False.
self.failIf(RegistrationProfile.objects.activate_user(sha.new('foo').hexdigest()))
def test_account_expiration_condition(self):
"""
Test that ``RegistrationProfile.activation_key_expired()``
returns ``True`` for expired users and for active users, and
``False`` otherwise.
"""
# Unexpired user returns False.
self.failIf(RegistrationProfile.objects.get(user=self.sample_user).activation_key_expired())
# Expired user returns True.
self.failUnless(RegistrationProfile.objects.get(user=self.expired_user).activation_key_expired())
# Activated user returns True.
RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.sample_user).activation_key)
self.failUnless(RegistrationProfile.objects.get(user=self.sample_user).activation_key_expired())
def test_expired_user_deletion(self):
"""
Test that
``RegistrationProfile.objects.delete_expired_users()`` deletes
only inactive users whose activation window has expired.
"""
RegistrationProfile.objects.delete_expired_users()
self.assertEqual(RegistrationProfile.objects.count(), 1)
def test_management_command(self):
"""
Test that ``manage.py cleanupregistration`` functions
correctly.
"""
management.call_command('cleanupregistration')
self.assertEqual(RegistrationProfile.objects.count(), 1)
class RegistrationFormTests(RegistrationTestCase):
"""
Tests for the forms and custom validation logic included in
django-registration.
"""
def test_registration_form(self):
"""
Test that ``RegistrationForm`` enforces username constraints
and matching passwords.
"""
invalid_data_dicts = [
# Non-alphanumeric username.
{
'data':
{ 'username': 'foo/bar',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' },
'error':
('username', [u"Enter a valid value."])
},
# Already-existing username.
{
'data':
{ 'username': 'alice',
'email': 'alice@example.com',
'password1': 'secret',
'password2': 'secret' },
'error':
('username', [u"This username is already taken. Please choose another."])
},
# Mismatched passwords.
{
'data':
{ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'bar' },
'error':
('__all__', [u"You must type the same password each time"])
},
]
for invalid_dict in invalid_data_dicts:
form = forms.RegistrationForm(data=invalid_dict['data'])
self.failIf(form.is_valid())
self.assertEqual(form.errors[invalid_dict['error'][0]], invalid_dict['error'][1])
form = forms.RegistrationForm(data={ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' })
self.failUnless(form.is_valid())
def test_registration_form_tos(self):
"""
Test that ``RegistrationFormTermsOfService`` requires
agreement to the terms of service.
"""
form = forms.RegistrationFormTermsOfService(data={ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' })
self.failIf(form.is_valid())
self.assertEqual(form.errors['tos'], [u"You must agree to the terms to register"])
form = forms.RegistrationFormTermsOfService(data={ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo',
'tos': 'on' })
self.failUnless(form.is_valid())
def test_registration_form_unique_email(self):
"""
Test that ``RegistrationFormUniqueEmail`` validates uniqueness
of email addresses.
"""
form = forms.RegistrationFormUniqueEmail(data={ 'username': 'foo',
'email': 'alice@example.com',
'password1': 'foo',
'password2': 'foo' })
self.failIf(form.is_valid())
self.assertEqual(form.errors['email'], [u"This email address is already in use. Please supply a different email address."])
form = forms.RegistrationFormUniqueEmail(data={ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' })
self.failUnless(form.is_valid())
def test_registration_form_no_free_email(self):
"""
Test that ``RegistrationFormNoFreeEmail`` disallows
registration with free email addresses.
"""
base_data = { 'username': 'foo',
'password1': 'foo',
'password2': 'foo' }
for domain in ('aim.com', 'aol.com', 'email.com', 'gmail.com',
'googlemail.com', 'hotmail.com', 'hushmail.com',
'msn.com', 'mail.ru', 'mailinator.com', 'live.com'):
invalid_data = base_data.copy()
invalid_data['email'] = u"foo@%s" % domain
form = forms.RegistrationFormNoFreeEmail(data=invalid_data)
self.failIf(form.is_valid())
self.assertEqual(form.errors['email'], [u"Registration using free email addresses is prohibited. Please supply a different email address."])
base_data['email'] = 'foo@example.com'
form = forms.RegistrationFormNoFreeEmail(data=base_data)
self.failUnless(form.is_valid())
class RegistrationViewTests(RegistrationTestCase):
"""
Tests for the views included in django-registration.
"""
def test_registration_view(self):
"""
Test that the registration view rejects invalid submissions,
and creates a new user and redirects after a valid submission.
"""
# Invalid data fails.
response = self.client.post(reverse('registration_register'),
data={ 'username': 'alice', # Will fail on username uniqueness.
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' })
self.assertEqual(response.status_code, 200)
self.failUnless(response.context['form'])
self.failUnless(response.context['form'].errors)
response = self.client.post(reverse('registration_register'),
data={ 'username': 'foo',
'email': 'foo@example.com',
'password1': 'foo',
'password2': 'foo' })
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver%s' % reverse('registration_complete'))
self.assertEqual(RegistrationProfile.objects.count(), 3)
def test_activation_view(self):
"""
Test that the activation view activates the user from a valid
key and fails if the key is invalid or has expired.
"""
# Valid user puts the user account into the context.
response = self.client.get(reverse('registration_activate',
kwargs={ 'activation_key': RegistrationProfile.objects.get(user=self.sample_user).activation_key }))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['account'].pk, self.sample_user.pk)
# Expired user sets the account to False.
response = self.client.get(reverse('registration_activate',
kwargs={ 'activation_key': RegistrationProfile.objects.get(user=self.expired_user).activation_key }))
self.failIf(response.context['account'])
# Invalid key gets to the view, but sets account to False.
response = self.client.get(reverse('registration_activate',
kwargs={ 'activation_key': 'foo' }))
self.failIf(response.context['account'])
# Nonexistent key sets the account to False.
response = self.client.get(reverse('registration_activate',
kwargs={ 'activation_key': sha.new('foo').hexdigest() }))
self.failIf(response.context['account'])

72
apps/registration/urls.py Normal file
View file

@ -0,0 +1,72 @@
"""
URLConf for Django user registration and authentication.
If the default behavior of the registration views is acceptable to
you, simply use a line like this in your root URLConf to set up the
default URLs for registration::
(r'^accounts/', include('registration.urls')),
This will also automatically set up the views in
``django.contrib.auth`` at sensible default locations.
But if you'd like to customize the behavior (e.g., by passing extra
arguments to the various views) or split up the URLs, feel free to set
up your own URL patterns for these views instead. If you do, it's a
good idea to use the names ``registration_activate``,
``registration_complete`` and ``registration_register`` for the
various steps of the user-signup process.
"""
from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
from django.contrib.auth import views as auth_views
from apps.registration.views import activate
from apps.registration.views import register
urlpatterns = patterns('apps.registration.views',
# Activation keys get matched by \w+ instead of the more specific
# [a-fA-F0-9]{40} because a bad activation key should still get to the view;
# that way it can return a sensible "invalid key" message instead of a
# confusing 404.
url(r'^activate/(?P<activation_key>\w+)/$',
'activate',
name='registration_activate'),
url(r'^login/$',
auth_views.login,
{'template_name': 'registration/login.html'},
name='auth_login'),
url(r'^logout/$',
auth_views.logout,
{'template_name': 'registration/login.html'},
name='auth_logout'),
url(r'^password/change/$',
auth_views.password_change,
name='auth_password_change'),
url(r'^password/change/done/$',
auth_views.password_change_done,
name='auth_password_change_done'),
url(r'^password/reset/$',
auth_views.password_reset,
name='auth_password_reset'),
url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
auth_views.password_reset_confirm,
name='auth_password_reset_confirm'),
url(r'^password/reset/complete/$',
auth_views.password_reset_complete,
name='auth_password_reset_complete'),
url(r'^password/reset/done/$',
auth_views.password_reset_done,
name='auth_password_reset_done'),
url(r'^register/$',
'register',
name='registration_register'),
url(r'^register/complete/$',
direct_to_template,
{'template': 'registration/registration_complete.html'},
name='registration_complete'),
)

164
apps/registration/views.py Normal file
View file

@ -0,0 +1,164 @@
"""
Views which allow users to create and activate accounts.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from apps.registration.forms import RegistrationForm
from apps.registration.models import RegistrationProfile
def activate(request, activation_key,
template_name='registration/activate.html',
extra_context=None):
"""
Activate a ``User``'s account from an activation key, if their key
is valid and hasn't expired.
By default, use the template ``registration/activate.html``; to
change this, pass the name of a template as the keyword argument
``template_name``.
**Required arguments**
``activation_key``
The activation key to validate and use for activating the
``User``.
**Optional arguments**
``extra_context``
A dictionary of variables to add to the template context. Any
callable object in this dictionary will be called to produce
the end result which appears in the context.
``template_name``
A custom template to use.
**Context:**
``account``
The ``User`` object corresponding to the account, if the
activation was successful. ``False`` if the activation was not
successful.
``expiration_days``
The number of days for which activation keys stay valid after
registration.
Any extra variables supplied in the ``extra_context`` argument
(see above).
**Template:**
registration/activate.html or ``template_name`` keyword argument.
"""
activation_key = activation_key.lower() # Normalize before trying anything with it.
account = RegistrationProfile.objects.activate_user(activation_key)
if extra_context is None:
extra_context = {}
context = RequestContext(request)
for key, value in extra_context.items():
context[key] = callable(value) and value() or value
return render_to_response(template_name,
{ 'account': account,
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS },
context_instance=context)
def register(request, success_url=None,
form_class=RegistrationForm, profile_callback=None,
template_name='registration/registration_form.html',
extra_context=None):
"""
Allow a new user to register an account.
Following successful registration, issue a redirect; by default,
this will be whatever URL corresponds to the named URL pattern
``registration_complete``, which will be
``/accounts/register/complete/`` if using the included URLConf. To
change this, point that named pattern at another URL, or pass your
preferred URL as the keyword argument ``success_url``.
By default, ``registration.forms.RegistrationForm`` will be used
as the registration form; to change this, pass a different form
class as the ``form_class`` keyword argument. The form class you
specify must have a method ``save`` which will create and return
the new ``User``, and that method must accept the keyword argument
``profile_callback`` (see below).
To enable creation of a site-specific user profile object for the
new user, pass a function which will create the profile object as
the keyword argument ``profile_callback``. See
``RegistrationManager.create_inactive_user`` in the file
``models.py`` for details on how to write this function.
By default, use the template
``registration/registration_form.html``; to change this, pass the
name of a template as the keyword argument ``template_name``.
**Required arguments**
None.
**Optional arguments**
``form_class``
The form class to use for registration.
``extra_context``
A dictionary of variables to add to the template context. Any
callable object in this dictionary will be called to produce
the end result which appears in the context.
``profile_callback``
A function which will be used to create a site-specific
profile instance for the new ``User``.
``success_url``
The URL to redirect to on successful registration.
``template_name``
A custom template to use.
**Context:**
``form``
The registration form.
Any extra variables supplied in the ``extra_context`` argument
(see above).
**Template:**
registration/registration_form.html or ``template_name`` keyword
argument.
"""
if request.method == 'POST':
form = form_class(data=request.POST, files=request.FILES)
if form.is_valid():
new_user = form.save(profile_callback=profile_callback)
# success_url needs to be dynamically generated here; setting a
# a default value using reverse() will cause circular-import
# problems with the default URLConf for this application, which
# imports this file.
return HttpResponseRedirect(success_url or reverse('registration_complete'))
else:
form = form_class()
if extra_context is None:
extra_context = {}
context = RequestContext(request)
for key, value in extra_context.items():
context[key] = callable(value) and value() or value
return render_to_response(template_name,
{ 'form': form },
context_instance=context)

View file

6
apps/rss_feeds/admin.py Normal file
View file

@ -0,0 +1,6 @@
from apps.rss_feeds.models import Feed, Story, Tag
from django.contrib import admin
admin.site.register(Feed)
admin.site.register(Story)
admin.site.register(Tag)

244
apps/rss_feeds/models.py Normal file
View file

@ -0,0 +1,244 @@
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from utils import feedparser, object_manager
from utils.dateutil.parser import parse as dateutil_parse
from utils.feed_functions import encode, prints, mtime
import time, datetime, random
from pprint import pprint
from django.utils.http import urlquote
from django.db.models import Q
from utils.diff import HTMLDiff
USER_AGENT = 'NewsBlur v1.0 - newsblur.com'
class Feed(models.Model):
feed_address = models.URLField(max_length=255, verify_exists=True, unique=True)
feed_link = models.URLField(max_length=200, blank=True)
feed_title = models.CharField(max_length=255, blank=True)
active = models.BooleanField(default=True)
num_subscribers = models.IntegerField(default=0)
last_update = models.DateTimeField(auto_now=True, default=0)
min_to_decay = models.IntegerField(default=15)
days_to_trim = models.IntegerField(default=90)
creation = models.DateField(auto_now_add=True)
etag = models.CharField(max_length=50, blank=True)
last_modified = models.DateTimeField(null=True, blank=True)
def __unicode__(self):
return self.feed_title
def last_updated(self):
return time.time() - time.mktime(self.last_update.timetuple())
def new_stories_since_date(self, date):
story_count = Story.objects.filter(story_date__gte=date,
story_feed=self).count()
return story_count
def add_feed(self, feed_address, feed_link, feed_title):
print locals()
def update(self, force=False, feed=None):
if (self.last_updated() / 60) < (self.min_to_decay + (random.random()*self.min_to_decay)) and not force:
print 'Feed unchanged: ' + self.feed_title
return
feed_updated, feed = cache.get("feed:" + self.feed_address, (None, None,))
if feed and not force:
print 'Feed Cached: ' + self.feed_title
if not feed or force:
last_modified = None
now = datetime.datetime.now()
if self.last_modified:
last_modified = datetime.datetime.timetuple(self.last_modified)
if not feed:
print '[%d] Retrieving Feed: %s %s' % (self.id, self.feed_title, last_modified)
feed = feedparser.parse(self.feed_address,
etag=self.etag,
modified=last_modified,
agent=USER_AGENT)
cache.set("feed:" + self.feed_address, (now, feed),
self.min_to_decay * 60 * 5)
self.last_update = datetime.datetime.now()
# check for movement or disappearance
if hasattr(feed, 'status'):
if feed.status == 301:
self.feed_url = feed.href
if feed.status == 410:
self.active = False
if feed.status >= 400:
return
# Fill in optional fields
if not self.feed_title:
self.feed_title = feed.feed.get('title',
feed.feed.get('link', 'No Title'))
if not self.feed_link:
self.feed_link = feed.feed.get('link', 'null:')
self.etag = feed.get('etag', '')
if not self.etag:
self.etag = ''
self.last_modified = mtime(feed.get('modified', datetime.datetime.timetuple(datetime.datetime.now())))
self.save()
for story in feed['entries']:
self.save_story(story)
self.trim_feed();
return
def trim_feed(self):
date_diff = datetime.datetime.now() - datetime.timedelta(self.days_to_trim)
stories = Story.objects.filter(story_feed=self, story_date__lte=date_diff)
for story in stories:
story.story_past_trim_date = True
story.save()
def save_story(self, story):
story = self._pre_process_story(story)
if story.get('title'):
story_contents = story.get('content')
if story_contents is not None:
story_content = story_contents[0]['value']
else:
story_content = story.get('summary')
existing_story = self._exists_story(story)
if not existing_story:
pub_date = datetime.datetime.timetuple(story.get('published'))
print '- New story: %s %s' % (pub_date, story.get('title'))
s = Story(story_feed = self,
story_date = story.get('published'),
story_title = story.get('title'),
story_content = story_content,
story_author = story.get('author'),
story_permalink = story.get('link')
)
try:
s.save()
except:
pass
elif existing_story.story_title != story.get('title') \
or existing_story.story_content != story_content:
# update story
print '- Updated story in feed (%s): %s / %s' % (self.feed_title, len(existing_story.story_content), len(story_content))
original_content = None
if existing_story.story_original_content:
original_content = existing_story.story_original_content
else:
original_content = existing_story.story_content
diff = HTMLDiff(original_content, story_content)
print "\t\tDiff: %s %s %s" % diff.getStats()
print '\tExisting title / New: : \n\t\t- %s\n\t\t- %s' % (existing_story.story_title, story.get('title'))
s = Story(id = existing_story.id,
story_feed = self,
story_date = story.get('published'),
story_title = story.get('title'),
story_content = diff.getDiff(),
story_original_content = original_content,
story_author = story.get('author'),
story_permalink = story.get('link')
)
try:
s.save()
except:
pass
# else:
# print "Unchanged story: %s " % story.get('title')
return
def _exists_story(self, entry):
pub_date = entry['published']
start_date = pub_date - datetime.timedelta(hours=12)
end_date = pub_date + datetime.timedelta(hours=12)
# print "Dates: %s %s %s" % (pub_date, start_date, end_date)
existing_story = Story.objects.filter(
(
Q(story_title__iexact=entry['title'])
& Q(story_date__range=(start_date, end_date))
)
| (
Q(story_permalink__iexact=entry['link'])
& Q(story_date__range=(start_date, end_date))
),
story_feed = self
)
if len(existing_story):
return existing_story[0]
else:
return None
def _pre_process_story(self, entry):
date_published = entry.get('published', entry.get('updated'))
if not date_published:
date_published = str(datetime.datetime.now())
date_published = dateutil_parse(date_published)
# Change the date to UTC and remove timezone info since
# MySQL doesn't support it.
timezone_diff = datetime.datetime.utcnow() - datetime.datetime.now()
date_published_offset = date_published.utcoffset()
if date_published_offset:
date_published = (date_published - date_published_offset
- timezone_diff).replace(tzinfo=None)
else:
date_published = date_published.replace(tzinfo=None)
entry['published'] = date_published
protocol_index = entry['link'].find("://")
if protocol_index != -1:
entry['link'] = (entry['link'][:protocol_index+3]
+ urlquote(entry['link'][protocol_index+3:]))
else:
entry['link'] = urlquote(entry['link'])
return entry
class Meta:
db_table="feeds"
ordering=["feed_title"]
class Tag(models.Model):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
def save(self):
super(Tag, self).save()
class Story(models.Model):
'''A feed item'''
story_feed = models.ForeignKey(Feed)
story_date = models.DateTimeField()
story_title = models.CharField(max_length=255)
story_content = models.TextField(null=True, blank=True)
story_original_content = models.TextField(null=True, blank=True)
story_content_type = models.CharField(max_length=255, null=True,
blank=True)
story_author = models.CharField(max_length=255, null=True, blank=True)
story_permalink = models.CharField(max_length=1000)
story_past_trim_date = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag)
def __unicode__(self):
return self.story_title
class Meta:
verbose_name_plural = "stories"
verbose_name = "story"
db_table="stories"
ordering=["-story_date", "story_feed"]

0
apps/rss_feeds/tests.py Normal file
View file

1
apps/rss_feeds/views.py Normal file
View file

@ -0,0 +1 @@
# Create your views here.

11
manage.py Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

509
media/css/reader.css Normal file
View file

@ -0,0 +1,509 @@
/* ========== */
/* = Global = */
/* ========== */
body {
/*resets*/margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none;
font-family: 'Lucida Grande',Helvetica, Arial;
font-size:62%;
}
a, a:active, a:hover, a:visited, button {
outline: none;
}
a img {
border: none;
}
/* =================== */
/* = Resize Controls = */
/* =================== */
.ui-layout-resizer-west {
background: #e0e0e0 url(../img/reader/resize_west_small.png) repeat-y 50% 50%;
}
.ui-layout-resizer-north {
background: #e0e0e0 url(../img/reader/resize_north.png) repeat-x 50% 50%;
}
/* ============= */
/* = Feed List = */
/* ============= */
#feed_list {
position: fixed;
left: 0px;
top: 0px;
width: 220px;
background-color: #D7DDE6;
border-right: 1px solid #808080;
z-index: 20;
font-size: 1.05em;
overflow-y: auto;
}
#feed_list .folder {
margin: 6px 0px 4px;
background: transparent url(../img/icons/silk/folder.png) no-repeat 3px 1px;
}
#feed_list .folder .folder_title {
padding: 3px 0px 4px 22px;
font-weight: bold;
display: block;
color: #404040;
text-transform: uppercase;
}
#feed_list .feed {
position: relative;
cursor: pointer;
border-top: 1px solid #D7DDE6;
border-bottom: 1px solid #D7DDE6;
}
#feed_list .feed_id {
display: none;
}
#feed_list img.feed_favicon {
position: absolute;
top: 2px;
left: 24px;
}
#feed_list .feed_title {
display: block;
font-weight: bold;
padding: 4px 42px 2px 45px;
text-decoration: none;
color: #272727;
line-height: 1.3em;
}
#feed_list .feed.no_unread_items .feed_title {
font-weight: normal;
}
#feed_list .feed.selected {
background: #f6a828 url(../theme/images/ui-bg_highlight-hard_35_f6a828_1x100.png) 0 50% repeat-x;
border-top: 1px solid #A8A8A8;
border-bottom: 1px solid #A8A8A8;
}
#feed_list .unread_count {
position: absolute;
top: 3px;
right: 7px;
font-weight: bold;
color: #FFF;
padding: 0 6px;
background-color: #8eb6e8;
}
/* ================ */
/* = Story Titles = */
/* ================ */
#story_titles {
z-index: 10;
position: fixed;
top: 0px;
left: 0px;
height: 200px;
width: 100%;
overflow-y: scroll;
font-size: 1.1em;
}
#story_titles .wrapper {
margin-left: 220px;
}
#story_titles .feed_bar {
font-weight: bold;
font-size: 1.3em;
padding: 2px 140px 2px 28px;
background: #dadada url(../theme/images/dadada_40x100_textures_03_highlight_soft_75.png) 0 50% repeat-x;
border-bottom: 2px solid #404040;
position: relative;
}
#story_titles .feed_bar .feed_heading {
display: block;
text-align: center;
}
#story_titles .feed_bar .feed_heading .feed_favicon {
margin-right: 8px;
vertical-align: middle;
}
#story_titles .feed_bar .unread_count {
position: absolute;
right: 4px;
top: 4px;
}
#story_titles .feed_bar .feed_id {
display: none;
}
#story_titles .story {
position: relative;
cursor: pointer;
font-weight: bold;
padding: 0px 206px 0px 28px;
border-top: 1px solid #E7EDF6;
text-decoration: none;
color: #272727;
line-height: 1em;
background: transparent url(../img/icons/silk/bullet_orange.png) no-repeat 6px 50%;
}
#story_titles .story.NB-story-hover {
background-color: #f0f0f0;
}
#story_titles .story a.story_title {
text-decoration: none;
color: #272727;
display: block;
padding: 4px 0px;
}
#story_titles .story .story_id {
display: none;
}
#story_titles .story.read {
font-weight: normal;
background: none;
}
#story_titles .story .story_date {
position: absolute;
right: 4px;
width: 200px;
top: 4px;
}
#story_titles .story.selected {
color: #304080;
border-top: 1px solid #D7DDE6;
background: #dadada url(../theme/images/dadada_40x100_textures_03_highlight_soft_75.png) 0 50% repeat-x;
}
#story_titles .story.after_selected {
border-top: 1px solid #D7DDE6;
}
/* =================== */
/* = Story Navigator = */
/* =================== */
#story_pane .story_navigator {
position: absolute;
right: 4px;
top: 6px;
}
#story_pane a.button {
outline: none;
border: none;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
vertical-align: middle;
display: -moz-inline-box;
display: inline-block;
overflow: visible;
color: #000;
background-color: #acc;
padding: 5px 7px;
margin: 0px 2px;
}
#story_pane a.button:hover {
background-color: #cee;
}
#story_pane a.button.next_unread {
float: right;
width: 24px;
height: 16px;
background: #acc url(../img/icons/silk/arrow_down.png) no-repeat 50% 50%;
}
#story_pane a.button.next_unread:hover {
background: #cee url(../img/icons/silk/arrow_down.png) no-repeat 50% 50%;
}
#story_pane a.button.previous_unread {
float: right;
width: 16px;
height: 16px;
background: #acc url(../img/icons/silk/arrow_up.png) no-repeat 50% 50%;
}
#story_pane a.button.previous_unread:hover {
background: #cee url(../img/icons/silk/arrow_up.png) no-repeat 50% 50%;
}
/* ================= */
/* = Story Content = */
/* ================= */
#story_pane {
z-index: 10;
position: fixed;
top: 200px;
left: 0px;
width: 100%;
overflow-y: scroll;
font-size: 1.2em;
line-height: 1.5em;
height: 315px;
}
#story_pane .wrapper {
margin-left: 220px;
position: relative;
}
#story_pane .story_title {
font-weight: bold;
font-size: 1.3em;
padding: 12px 140px 12px 28px;
background: #dadada url(../theme/images/dadada_40x100_textures_03_highlight_soft_75.png) 0 50% repeat-x;
border-top: 4px solid #404040;
}
#story_pane .story_title a {
text-decoration: none;
color: #101050;
}
#story_pane .story_title a:hover {
color: #1010A0;
}
#story_pane .story_meta {
color: #606060;
font-weight: bold;
font-size: .8em;
width: 6em;
line-height: 1.9em;
clear: both;
float: left;
text-transform: uppercase;
padding: 0px 4px 0px 0px;
}
#story_pane .story_feed {
padding: 4px 140px 0px 28px;
}
#story_pane .story_feed .feed_favicon {
position: absolute;
left: 0px;
top: -1px;
}
#story_pane .story_feed .data {
padding-left: 20px;
position: relative;
}
#story_pane .story_author {
padding: 0px 140px 0px 28px;
}
#story_pane .story_date {
padding: 0px 140px 0px 28px;
}
#story_pane .story_content {
border-top: 1px solid #909090;
margin: 12px 140px 24px 28px;
padding: 12px 0px;
}
#story_pane .story_endbar {
height: 8px;
border-top: 1px solid #404040;
background: #dadada url(../theme/images/dadada_40x100_textures_03_highlight_soft_75.png) 0 50% repeat-x;
}
/* ======================= */
/* = Story Modifications = */
/* ======================= */
#story_pane {
color: #2b2b2b;
}
#story_pane p {
clear: both;
}
#story_pane blockquote {
background-color: #F0F0F0;
border-left: 1px solid #9B9B9B;
padding: .5em 2em;
margin: 0px;
}
/* ============ */
/* = Task Bar = */
/* ============ */
#task_bar {
height: 29px;
background: #e0e0e0 url(../img/reader/taskbar_background.png) repeat-x top left;
}
/* ==================== */
/* = OPML Import Form = */
/* ==================== */
form.opml_import_form {
}
form.opml_import_form textarea {
width: 100%;
height: 200px;
}
form.opml_import_form .section {
clear: both;
margin: 2px 0px;
}
form.opml_import_form label {
display: block;
}
form.opml_import_form input {
display: block;
clear: both;
float: left;
margin: 0px 4px;
}
/* ============== */
/* = Bottom Bar = */
/* ============== */
/*************************/
/* Recommended for menus */
/*************************/
button.menu {
cursor: pointer;
}
div.menu {
display: none;
z-index: 99;
position: absolute;
}
div.menu.active {
z-index: 100;
}
/***********************/
/* Completely optional */
/***********************/
.menu_button {
line-height: 1.8;
text-shadow: 1px 1px #ddd;
color: #222;
background: #a8acae;
margin: 3px 2em 0 0.5em;
padding: 0 0.8em;
border: 0;
border-bottom: 1px solid #686c6e;
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
float: right;
position: relative;
cursor: pointer;
}
.menu_button img {
vertical-align: -3px;
margin-right: 0.2em;
}
.menu_button .menu {
display: none;
right: 0px;
bottom: 20px;
cursor: normal;
}
.menu_button.hover .menu {
display: block;
}
.menu_button.hover {
-webkit-border-top-left-radius: 0;
-webkit-border-top-right-radius: 0;
-moz-border-radius-topleft: 0;
-moz-border-radius-topright: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top-color: #cacece;
}
button.menu:hover,
button.menu.active {
background-color: #cacece;
text-shadow: 1px 1px #fff;
}
div.menu {
font-size: 88%;
color: #444;
background: #cacece;
padding: 1em;
border: 1px solid #cacece;
border-top-width: 3px;
-moz-border-radius: 0.5em;
-moz-border-radius-topleft: 0;
-webkit-border-radius: 0.5em;
-webkit-border-top-left-radius: 0;
border-radius: 0.5em;
border-top-left-radius: 0;
}
div.menu.active {
background-color: #eaeeee;
}
div.menu h3 {
font-size: 108%;
font-weight: bold;
color: #444;
margin: 0;
}
div.menu h4 {
font-size: 100%;
font-weight: normal;
line-height: 1.5;
color: #999;
margin: 0 0 0.5em 0;
white-space: nowrap;
}
div.menu hr {
border: 0;
border-top: 1px solid #ddd;
border-bottom: 1px solid #fff;
}
div.menu ul {
padding-left: 1.5em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
media/img/icons/actions/add.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

BIN
media/img/icons/actions/add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

BIN
media/img/icons/actions/copy.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

BIN
media/img/icons/actions/copy.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

BIN
media/img/icons/actions/cut.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

BIN
media/img/icons/actions/cut.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

BIN
media/img/icons/actions/info.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

BIN
media/img/icons/actions/info.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
media/img/icons/actions/lock.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

BIN
media/img/icons/actions/lock.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

BIN
media/img/icons/actions/ok.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

BIN
media/img/icons/actions/ok.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
media/img/icons/actions/paste.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

BIN
media/img/icons/actions/paste.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

BIN
media/img/icons/actions/stop.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

BIN
media/img/icons/actions/stop.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

BIN
media/img/icons/actions/tab.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

BIN
media/img/icons/actions/tab.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

BIN
media/img/icons/actions/tabs.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Some files were not shown because too many files have changed in this diff Show more