diff --git a/.gitignore b/.gitignore index d8c3eed..20f1146 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ codesters/prod_settings.py *.db assets/* media/* -gunicorn.conf.py +gunicorn-codesters.conf.py +.idea \ No newline at end of file diff --git a/README.md b/README.md index a209f3b..d11316c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We are developing knowledge library for learning programming. The best resources ##Tools/Apps Used -+ [Django 1.4.5](https://www.djangoproject.com/) ++ [Django 1.6](https://www.djangoproject.com/) + [Twitter Bootstrap](http://getbootstrap.com/) + [django-registration](https://django-registration.readthedocs.org/en/latest/) + [django-guardian](https://github.com/lukaszb/django-guardian) @@ -43,11 +43,12 @@ See INSTALL.md for full installation instructions. + [Karambir Singh Nain](http://nainomics.in/) + [Mohammad Adil](http://madil.in/) + [Mayank Jain](http://mayank-jain.in/) ++ Naman Sharma ## We need your help -+ We need Django developers for this project. -+ None of us are a designer, if you can help or know someone who can, please [mail us](mailto:akarambir@gmail.com). ++ We are porting this project to Django 1.8 and rewriting the way the resources are categorised into types and topics. We need Django developers for this. ++ None of us are a designer, if you can help or know someone who can, please [mail us](mailto:karambir@codesters.org). ## LICENSE diff --git a/codesters/settings.py b/codesters/settings.py index f9dc37b..385b0ba 100644 --- a/codesters/settings.py +++ b/codesters/settings.py @@ -7,8 +7,6 @@ ADMINS = ( ('Karambir Singh Nain', 'karambir@codesters.org'), - ('Mohammad Adil', 'adil@codesters.org'), - ('Mayank Jain', 'mayank@codesters.org'), ) MANAGERS = ADMINS @@ -22,14 +20,15 @@ 'django.contrib.staticfiles', 'django.contrib.admin', 'django.contrib.sitemaps', - 'django.contrib.markup', + # 'django.contrib.markup', + 'django.contrib.flatpages', ) THIRD_PARTY_APPS = ( + 'markdown_deux', 'haystack', - 'south', + # 'south', 'django_extensions', - 'registration', 'guardian', 'widget_tweaks', 'braces', @@ -38,6 +37,7 @@ ) LOCAL_APPS = ( + 'registration', 'profiles', # 'tracks', 'resources', @@ -47,8 +47,8 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': '', # Or path to database file if using sqlite3. + 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'codesters', # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. @@ -106,7 +106,7 @@ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) TEMPLATE_CONTEXT_PROCESSORS = ( @@ -149,7 +149,7 @@ } #HAYSTACK settings -HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor' HAYSTACK_SEARCH_RESULTS_PER_PAGE = 12 HAYSTACK_CONNECTIONS = { 'default': { @@ -164,10 +164,11 @@ ABSOLUTE_URL_OVERRIDES = { 'auth.user': lambda u: "/profile/%s/" % u.username, } +SERVER_EMAIL = "admin@codesters.org" #Django-Registration Settings ACCOUNT_ACTIVATION_DAYS = 7 - +DEFAULT_FROM_EMAIL = "admin@codesters.org" #Django-Guardian Settings ANONYMOUS_USER_ID = -1 diff --git a/codesters/sitemaps.py b/codesters/sitemaps.py index 7eeea50..7563f0c 100644 --- a/codesters/sitemaps.py +++ b/codesters/sitemaps.py @@ -1,6 +1,5 @@ from django.contrib.sitemaps import GenericSitemap from resources.models import Resource, Topic -from profiles.models import Snippet resource_dict = { 'queryset': Resource.objects.filter(show=True), @@ -9,14 +8,9 @@ topic_dict = { 'queryset': Topic.objects.all(), } -snippet_dict = { - 'queryset': Snippet.objects.filter(show=True), - 'date_field': 'updated_at', -} sitemaps = { 'topic': GenericSitemap(topic_dict, priority=0.8), 'resource': GenericSitemap(resource_dict, priority=0.6), - 'snippet': GenericSitemap(snippet_dict, priority=0.6), } diff --git a/codesters/urls.py b/codesters/urls.py index dc06680..13b5516 100644 --- a/codesters/urls.py +++ b/codesters/urls.py @@ -12,9 +12,6 @@ urlpatterns = patterns('', url(r'^$', HomeView.as_view(), name='page_home'), - url(r'^about/$', AboutView.as_view(), name='page_about'), - url(r'^contact/$', ContactView.as_view(), name='page_contact'), - url(r'^guidelines/$', GuidelinesView.as_view(), name='page_guidelines'), url(r'^explore/$', explore_home, name='explore_home'), url(r'^admin/$', RedirectView.as_view(url='/', permanent=True)), url(r'^explore/resource/all/$', RecentResourceListView.as_view(), name='explore_recent_resources'), @@ -36,3 +33,16 @@ urlpatterns += patterns('', (r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}), ) + + +urlpatterns += patterns('django.contrib.flatpages.views', + url(r'^about/$', 'flatpage', {'url': '/about/'}, name='page_about'), + url(r'^contact/$', 'flatpage', {'url': '/contact/'}, name='page_contact'), + url(r'^guidelines/$', 'flatpage', {'url': '/guidelines/'}, name='page_guidelines'), + url(r'^license/$', 'flatpage', {'url': '/license/'}, name='page_license'), +) + +#Left for future, if needs to make many flatpages +#urlpatterns += patterns('', +# url(r'pages/', include('django.contrib.flatpages.urls')), +#) diff --git a/codesters/views.py b/codesters/views.py index 32e0537..5170ed8 100644 --- a/codesters/views.py +++ b/codesters/views.py @@ -7,7 +7,6 @@ from django.db.models import Count from django.contrib.auth.models import User from resources.models import Topic, Resource -from profiles.models import Snippet def popular_domains_mixin(number='more'): resources = Resource.objects.all() @@ -57,11 +56,8 @@ def explore_home(request): recent_resources = Resource.objects.all().order_by('-created_at')[:5] popular_resources = Resource.objects.all().order_by('-rating_votes')[:5] - recent_snippets = Snippet.objects.all().order_by('-updated_at')[:5] - ctx = { 'active_users': active_users[:4], - 'recent_snippets': recent_snippets, 'popular_topics': popular_topics[:5], 'popular_domains': popular_domains_mixin(number='less'), 'recent_resources': recent_resources, diff --git a/dev-requirements.txt b/dev-requirements.txt index 5160584..c986eef 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,30 +1,43 @@ -Django==1.4.5 -MySQL-python==1.2.4 -PIL==1.1.7 -South==0.7.6 -amqp==1.0.11 +amqp==1.4.8 anyjson==0.3.3 -argparse==1.2.1 -billiard==2.7.3.28 -celery==3.0.19 -distribute==0.6.36 -django-braces==1.0.0 -django-celery==3.0.17 -django-disqus==0.4.1 -django-extensions==1.0.3 -django-guardian +billiard==3.3.0.22 +celery==3.1.19 +certifi==2015.11.20.1 +decorator==4.0.6 +Django==1.6.11 +django-braces==1.8.1 +django-celery==3.1.17 +django-disqus==0.5 +django-extensions==1.6.1 +django-guardian==1.3.2 +django-haystack==2.4.1 +django-markdown-deux==1.0.5 django-ratings==0.3.7 -django-registration==0.8 -django-widget-tweaks==1.1.2 -gunicorn==0.17.2 -ipython==0.13.1 -kombu==2.5.10 -python-dateutil==2.1 -pytz==2013b -six==1.3.0 -wsgiref==0.1.2 -Markdown==2.3.1 -requests==1.2.3 -simplejson==3.3.0 -pyelasticsearch==0.5 -django-haystack==2.0.0 +django-registration==2.0.3 +django-widget-tweaks==1.4.1 +elasticsearch==1.9.0 +funcsigs==0.4 +gunicorn==19.4.1 +ipython==4.0.1 +ipython-genutils==0.1.0 +kombu==3.0.32 +Markdown==2.6.5 +markdown2==2.3.0 +mock==1.3.0 +path.py==8.1.2 +pbr==1.8.1 +pexpect==4.0.1 +pickleshare==0.5 +Pillow==3.0.0 +psycopg2==2.6.1 +ptyprocess==0.5 +pyelasticsearch==1.4 +python-dateutil==2.4.2 +pytz==2015.7 +requests==2.9.1 +simplegeneric==0.8.1 +simplejson==3.8.1 +six==1.10.0 +traitlets==4.0.0 +urllib3==1.13.1 +wheel==0.26.0 diff --git a/gunicorn-example.conf.py b/gunicorn-example.conf.py new file mode 100644 index 0000000..e69627d --- /dev/null +++ b/gunicorn-example.conf.py @@ -0,0 +1,2 @@ +bind = "127.0.0.1:8001" +workers = 2 diff --git a/profiles/admin.py b/profiles/admin.py index 67540f6..932b14f 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import UserProfile, Snippet, Project, SavedResource, TopicFollow +from .models import UserProfile, Project, SavedResource, TopicFollow class UserProfileAdmin(admin.ModelAdmin): list_display = ('user', 'receive_email',) @@ -14,20 +14,11 @@ class TopicFollowAdmin(admin.ModelAdmin): list_display = ('user', 'topic', 'followed_at') date_hierarchy = 'followed_at' -class SnippetAdmin(admin.ModelAdmin): - list_display=('show', 'title', 'user', ) - list_display_links = ['title'] - list_editable = ['show'] - date_hierarchy = 'created_at' - list_filter = ['show'] - search_fields = ['title', 'content'] - class ProjectAdmin(admin.ModelAdmin): list_display=('title', 'user', ) search_fields = ['title', 'description', 'url', 'source_url'] admin.site.register(UserProfile, UserProfileAdmin) -admin.site.register(Snippet, SnippetAdmin) admin.site.register(Project, ProjectAdmin) admin.site.register(SavedResource, SavedResourceAdmin) admin.site.register(TopicFollow, TopicFollowAdmin) diff --git a/profiles/feeds.py b/profiles/feeds.py deleted file mode 100644 index 7c8cb51..0000000 --- a/profiles/feeds.py +++ /dev/null @@ -1,48 +0,0 @@ -from django.contrib.syndication.views import Feed -from django.utils.feedgenerator import Atom1Feed -from django.shortcuts import get_object_or_404 -from django.contrib.auth.models import User -from .models import Snippet - -class RecentSnippetsRss(Feed): - title = "Recent Snippets on Codesters" - description = "Provides feed for recent snippets shared on codesters.org" - description_template = "feeds/snippets.html" - - def items(self): - return Snippet.objects.order_by('-created_at') - - def item_title(self, item): - return item.title - - -class RecentSnippetsAtom(RecentSnippetsRss): - feed_type = Atom1Feed - subtitle = RecentSnippetsRss.description - - -class UserRecentSnippetsRss(Feed): - description_template = "feeds/snippets.html" - - def get_object(self, request, username): - return get_object_or_404(User, username=username) - - def title(self, obj): - return "Recent snippets from %s" %obj.username - - def link(self, obj): - return obj.get_absolute_url() - - def description(self, obj): - return "feed for recent snippets shared on codesters from %s" %obj.username - - def items(self, obj): - return obj.snippet_set.all().order_by('-created_at') - - def item_title(self, item): - return item.title - - -class UserRecentSnippetsAtom(UserRecentSnippetsRss): - feed_type = Atom1Feed - subtitle = UserRecentSnippetsRss.description diff --git a/profiles/forms.py b/profiles/forms.py index 549a235..05958eb 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -2,7 +2,7 @@ from django.forms import ModelForm from django.contrib.auth.models import User -from .models import UserProfile, Snippet, Project +from .models import UserProfile, Project class UserUpdateForm(ModelForm): class Meta: @@ -14,16 +14,6 @@ class Meta: model = UserProfile fields = ('github', 'twitter', 'stackoverflow', 'facebook', 'website', 'gravatar_email', 'bio') -class SnippetCreateForm(ModelForm): - class Meta: - model = Snippet - fields = ('title', 'content') - -class SnippetUpdateForm(ModelForm): - class Meta: - model = Snippet - fields = ('title', 'content') - class ProjectCreateForm(ModelForm): class Meta: model = Project diff --git a/profiles/migrations/0004_auto__del_snippet.py b/profiles/migrations/0004_auto__del_snippet.py new file mode 100644 index 0000000..34fde39 --- /dev/null +++ b/profiles/migrations/0004_auto__del_snippet.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting model 'Snippet' + db.delete_table(u'profiles_snippet') + + + def backwards(self, orm): + # Adding model 'Snippet' + db.create_table(u'profiles_snippet', ( + ('content', self.gf('django.db.models.fields.TextField')()), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('show', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('profiles', ['Snippet']) + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'profiles.project': { + 'Meta': {'object_name': 'Project'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'source_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '60'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'profiles.savedresource': { + 'Meta': {'unique_together': "(('user', 'resource'),)", 'object_name': 'SavedResource'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['resources.Resource']"}), + 'saved_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'profiles.topicfollow': { + 'Meta': {'unique_together': "(('user', 'topic'),)", 'object_name': 'TopicFollow'}, + 'followed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['resources.Topic']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'profiles.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'bio': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'facebook': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'github': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'gravatar_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'receive_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'stackoverflow': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + u'resources.resource': { + 'Meta': {'object_name': 'Resource'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'help_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'rating_score': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}), + 'rating_votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'resource_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['resources.ResourceType']"}), + 'show': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'topics': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['resources.Topic']", 'symmetrical': 'False'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}) + }, + u'resources.resourcetype': { + 'Meta': {'object_name': 'ResourceType'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'purple'", 'unique': 'True', 'max_length': '20'}), + 'help_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '60'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}) + }, + u'resources.topic': { + 'Meta': {'ordering': "['name']", 'object_name': 'Topic'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'help_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '60'}), + 'official_website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}), + 'thumbnail': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['profiles'] \ No newline at end of file diff --git a/profiles/models.py b/profiles/models.py index dd8c9b8..69effa2 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -15,22 +15,6 @@ def __unicode__(self): return self.title -class Snippet(models.Model): - title = models.CharField(max_length=255) - content = models.TextField() - show = models.BooleanField(default=True) - user = models.ForeignKey(User) - created_at = models.DateTimeField(auto_now_add=True, editable=False) - updated_at = models.DateTimeField(auto_now=True, editable=False) - - def __unicode__(self): - return self.title - - def get_absolute_url(self): - return reverse('snippet_detail', kwargs={'username': self.user.username, 'pk': self.pk}) - - - #TODO add a textfield for storing all social profiles at one place and write a method that returns a dict of provider with profile class UserProfile(models.Model): user = models.OneToOneField(User) @@ -80,9 +64,8 @@ def __unicode__(self): from django.contrib.auth.signals import user_logged_in from registration.signals import user_activated -@receiver(user_activated) def create_user_profile(sender, user, request, **kwargs): - user_profile = UserProfile.objects.create(user=user) + user_profile = UserProfile.objects.get_or_create(user=user)[0] from guardian.shortcuts import assign_perm assign_perm('change_userprofile', user, user_profile) assign_perm('delete_userprofile', user, user_profile) @@ -100,11 +83,7 @@ def create_project_permission(sender, instance, created, **kwargs): assign_perm('change_project', instance.user, instance) assign_perm('delete_project', instance.user, instance) -def create_snippet_permission(sender, instance, created, **kwargs): - if created: - assign_perm('change_snippet', instance.user, instance) - assign_perm('delete_snippet', instance.user, instance) +user_activated.connect(create_user_profile) user_logged_in.connect(check_userprofile_details) -post_save.connect(create_snippet_permission, sender=Snippet) post_save.connect(create_project_permission, sender=Project) diff --git a/profiles/search_indexes.py b/profiles/search_indexes.py index e0a3271..ddd5667 100644 --- a/profiles/search_indexes.py +++ b/profiles/search_indexes.py @@ -1,19 +1,6 @@ import datetime from haystack import indexes -from profiles.models import Snippet, Project - - -class SnippetIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - user = indexes.CharField(model_attr='user') - content = indexes.CharField(model_attr='content') - - def get_model(self): - return Snippet - - def index_queryset(self, using=None): - return self.get_model().objects.filter(show=True).filter(updated_at__lte=datetime.datetime.now()) - +from .models import Project class ProjectIndex(indexes.SearchIndex, indexes.Indexable): diff --git a/profiles/templates/profiles/snippet_detail.html b/profiles/templates/profiles/snippet_detail.html deleted file mode 100644 index 07b01cc..0000000 --- a/profiles/templates/profiles/snippet_detail.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} -{% load disqus_tags %} -{% load age from codesters %} -{% load markup %} - -{% block extra_head %} - - -{% endblock %} - -{% block content %} -
-
-

{{ snippet.user }} Wall

-
-
-
-
- -
-

{{ snippet.title }}

-

added {{ snippet.created_at|age }} -{% ifequal user snippet.user %} - Edit -{% endifequal %}

- -
- -
-{{ snippet.content|markdown:"safe" }} -{% include "snippets/share_this.html" %} -
- - - -
-{% endblock %} diff --git a/profiles/templates/profiles/snippet_form.html b/profiles/templates/profiles/snippet_form.html deleted file mode 100644 index 3afd82b..0000000 --- a/profiles/templates/profiles/snippet_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} -{% block content %} -
-

{{ headline }}

-
-
- {% include "snippets/forms/form_base_block.html" %} - -
-
-{% endblock %} diff --git a/profiles/templates/profiles/user_snippets.html b/profiles/templates/profiles/user_snippets.html deleted file mode 100644 index d5c7aef..0000000 --- a/profiles/templates/profiles/user_snippets.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "profiles/base.html" %} -{% load url from future %} - -{% load codesters %} -{% block profile_content %} -
- {% ifequal user userinfo %} - - {% endifequal %} -
- {% for snippet in snippets %} -

{{ snippet.title }} created {{ snippet.created_at|age }}

- {% ifequal user userinfo %} -
{% csrf_token %} - Edit - -
- {% endifequal %} - {% empty %} -

Wall is empty

- {% endfor %} - {% include "pagination.html" %} -
-
-{% endblock %} diff --git a/profiles/urls.py b/profiles/urls.py index ebe8835..c71a83c 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -1,7 +1,6 @@ from django.conf.urls import patterns, include, url from .views import * -from .feeds import UserRecentSnippetsRss, UserRecentSnippetsAtom urlpatterns = patterns('', url(r'^$', MyProfileView.as_view(), name='my_profile'), @@ -13,11 +12,4 @@ url(r'^(?P[\w.@+-]+)/project/new/$', ProjectCreateView.as_view(), name='project_create'), url(r'^(?P[\w.@+-]+)/project/(?P\d+)/edit/$', ProjectUpdateView.as_view(), name='project_update'), url(r'^(?P[\w.@+-]+)/project/(?P\d+)/delete/$', ProjectDeleteView.as_view(), name='project_delete'), - url(r'^(?P[\w.@+-]+)/snippets/$', UserSnippetsView.as_view(), name='user_snippets'), - url(r'^(?P[\w.@+-]+)/snippets/rss/$', UserRecentSnippetsRss(), name='user_snippet_rss'), - url(r'^(?P[\w.@+-]+)/snippets/atom/$', UserRecentSnippetsAtom(), name='user_snippet_atom'), - url(r'^(?P[\w.@+-]+)/snippets/new/$', SnippetCreateView.as_view(), name='snippet_create'), - url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/$', SnippetDetailView.as_view(), name='snippet_detail'), - url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/edit/$', SnippetUpdateView.as_view(), name='snippet_update'), - url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/delete/$', SnippetDeleteView.as_view(), name='snippet_delete'), ) diff --git a/profiles/views.py b/profiles/views.py index a09e91d..f82a0c7 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -7,10 +7,10 @@ from braces.views import SetHeadlineMixin from django.contrib.auth.models import User -from .models import UserProfile, Snippet, SavedResource, TopicFollow, Project +from .models import UserProfile, SavedResource, TopicFollow, Project from resources.models import Resource -from .forms import UserUpdateForm, UserProfileUpdateForm, SnippetCreateForm, SnippetUpdateForm, ProjectCreateForm, ProjectUpdateForm +from .forms import UserUpdateForm, UserProfileUpdateForm, ProjectCreateForm, ProjectUpdateForm def user_redirect_view(request, username): user = get_object_or_404(User, username=username) @@ -111,84 +111,6 @@ def get_success_url(self): return reverse_lazy('user_projects', kwargs={'username':self.request.user.username}) -class UserSnippetsView(SetHeadlineMixin, UserInfoMixin, ListView): - context_object_name = 'snippets' - template_name = 'profiles/user_snippets.html' - paginate_by = 12 - - def get_queryset(self): - user = get_object_or_404(User, username=self.kwargs['username']) - self.headline = unicode(user.username) + ' Wall' - return Snippet.objects.filter(user=user).filter(show=True) - -#TODO first implement a view_snippet permission in models then add a Permission required mixin here -class UserHiddenSnippetsView(SetHeadlineMixin, ListView): - context_object_name = 'snippets' - template_name = 'profiles/user_snippets.html' - paginate_by = 12 - - def get_queryset(self): - user = get_object_or_404(User, username=self.kwargs['username']) - return Snippet.objects.filter(user=user).filter(show=False) - - def get_context_data(self,**kwargs): - context = super(UserHiddenSnippetsView, self).get_context_data(**kwargs) - user = get_object_or_404(User, username=self.kwargs['username']) - context['userinfo'] = user - return context - - -class SnippetDetailView(SetHeadlineMixin, DetailView): - model = Snippet - context_object_name = 'snippet' - template_name = 'profiles/snippet_detail.html' - - def get_object(self): - snippet = get_object_or_404(Snippet, pk=self.kwargs['pk']) - if snippet.show: - self.headline = unicode(snippet.title) + """ | """ + unicode(self.request.user.username) + """ Wall""" - return snippet - else: - raise Http404 - - -class SnippetCreateView(LoginRequiredMixin, SetHeadlineMixin, CreateView): - model = Snippet - form_class = SnippetCreateForm - headline = 'Create New Snippet' - - def form_valid(self, form): - user = get_object_or_404(User, username =self.request.user.username) - form.instance.user = user - return super(SnippetCreateView, self).form_valid(form) - - def get_success_url(self): - messages.success(self.request, 'New Snippet have been created') - return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username}) - - -class SnippetUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SetHeadlineMixin, UpdateView): - model = Snippet - form_class = SnippetUpdateForm - permission_required = 'profiles.change_snippet' - headline = 'Edit Snippet' - return_403 = True - - def get_success_url(self): - messages.success(self.request, 'Your changes have been saved') - return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username}) - - -class SnippetDeleteView(PermissionRequiredMixin, DeleteView): - permission_required = 'profiles.delete_snippet' - return_403 = True - model = Snippet - - def get_success_url(self): - messages.success(self.request, 'Your snippet have been deleted') - return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username}) - - class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SetHeadlineMixin, UpdateView): form_class = UserUpdateForm model = User diff --git a/registration/__init__.py b/registration/__init__.py new file mode 100644 index 0000000..6e1954a --- /dev/null +++ b/registration/__init__.py @@ -0,0 +1,22 @@ +VERSION = (1, 0, 0, 'final', 0) + + +def get_version(): + "Returns a PEP 386-compliant version number from VERSION." + assert len(VERSION) == 5 + assert VERSION[3] in ('alpha', 'beta', 'rc', 'final') + + # Now build the two parts of the version number: + # main = X.Y[.Z] + # sub = .devN - for pre-alpha releases + # | {a|b|c}N - for alpha, beta and rc releases + + parts = 2 if VERSION[2] == 0 else 3 + main = '.'.join(str(x) for x in VERSION[:parts]) + + sub = '' + if VERSION[3] != 'final': + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + sub = mapping[VERSION[3]] + str(VERSION[4]) + + return str(main + sub) diff --git a/registration/admin.py b/registration/admin.py new file mode 100644 index 0000000..6541f6a --- /dev/null +++ b/registration/admin.py @@ -0,0 +1,46 @@ +from django.contrib import admin +from django.contrib.sites.models import RequestSite +from django.contrib.sites.models import Site +from django.utils.translation import ugettext_lazy as _ + +from registration.models import RegistrationProfile + + +class RegistrationAdmin(admin.ModelAdmin): + actions = ['activate_users', 'resend_activation_email'] + list_display = ('user', 'activation_key_expired') + raw_id_fields = ['user'] + search_fields = ('user__username', 'user__first_name', 'user__last_name') + + def activate_users(self, request, queryset): + """ + Activates the selected users, if they are not alrady + activated. + + """ + for profile in queryset: + RegistrationProfile.objects.activate_user(profile.activation_key) + activate_users.short_description = _("Activate users") + + def resend_activation_email(self, request, queryset): + """ + Re-sends activation emails for the selected users. + + Note that this will *only* send activation emails for users + who are eligible to activate; emails will not be sent to users + whose activation keys have expired or who have already + activated. + + """ + if Site._meta.installed: + site = Site.objects.get_current() + else: + site = RequestSite(request) + + for profile in queryset: + if not profile.activation_key_expired(): + profile.send_activation_email(site) + resend_activation_email.short_description = _("Re-send activation emails") + + +admin.site.register(RegistrationProfile, RegistrationAdmin) diff --git a/registration/auth_urls.py b/registration/auth_urls.py new file mode 100644 index 0000000..8515ee1 --- /dev/null +++ b/registration/auth_urls.py @@ -0,0 +1,60 @@ +""" +URL patterns for the views included in ``django.contrib.auth``. + +Including these URLs (via the ``include()`` directive) will set up the +following patterns based at whatever URL prefix they are included +under: + +* User login at ``login/``. + +* User logout at ``logout/``. + +* The two-step password change at ``password/change/`` and + ``password/change/done/``. + +* The four-step password reset at ``password/reset/``, + ``password/reset/confirm/``, ``password/reset/complete/`` and + ``password/reset/done/``. + +The default registration backend already has an ``include()`` for +these URLs, so under the default setup it is not necessary to manually +include these views. Other backends may or may not include them; +consult a specific backend's documentation for details. + +""" + +from django.conf.urls import include +from django.conf.urls import patterns +from django.conf.urls import url + +from django.contrib.auth import views as auth_views + + +urlpatterns = patterns('', + url(r'^login/$', + auth_views.login, + {'template_name': 'registration/login.html'}, + name='auth_login'), + url(r'^logout/$', + auth_views.logout, + {'template_name': 'registration/logout.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[0-9A-Za-z]+)-(?P.+)/$', + 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'), +) diff --git a/registration/backends/__init__.py b/registration/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/backends/default/__init__.py b/registration/backends/default/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/backends/default/urls.py b/registration/backends/default/urls.py new file mode 100644 index 0000000..c1251b8 --- /dev/null +++ b/registration/backends/default/urls.py @@ -0,0 +1,50 @@ +""" +URLconf for registration and activation, using django-registration's +default backend. + +If the default behavior of these 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.backends.default.urls')), + +This will also automatically set up the views in +``django.contrib.auth`` at sensible default locations. + +If you'd like to customize registration behavior, feel free to set up +your own URL patterns for these views instead. + +""" + + +from django.conf.urls import patterns +from django.conf.urls import include +from django.conf.urls import url +from django.views.generic.base import TemplateView + +from registration.backends.default.views import ActivationView +from registration.backends.default.views import RegistrationView + + +urlpatterns = patterns('', + url(r'^activate/complete/$', + TemplateView.as_view(template_name='registration/activation_complete.html'), + name='registration_activation_complete'), + # 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\w+)/$', + ActivationView.as_view(), + name='registration_activate'), + url(r'^register/$', + RegistrationView.as_view(), + name='registration_register'), + url(r'^register/complete/$', + TemplateView.as_view(template_name='registration/registration_complete.html'), + name='registration_complete'), +# url(r'^register/closed/$', +# TemplateView.as_view(template_name='registration/registration_closed.html'), +# name='registration_disallowed'), + (r'', include('registration.auth_urls')), + ) diff --git a/registration/backends/default/views.py b/registration/backends/default/views.py new file mode 100644 index 0000000..3cd8563 --- /dev/null +++ b/registration/backends/default/views.py @@ -0,0 +1,134 @@ +from django.conf import settings +from django.contrib.sites.models import RequestSite +from django.contrib.sites.models import Site + +from registration import signals +from registration.forms import RegistrationFormUniqueEmail +from registration.models import RegistrationProfile +from registration.views import ActivationView as BaseActivationView +from registration.views import RegistrationView as BaseRegistrationView + + +class RegistrationView(BaseRegistrationView): + """ + A registration backend which follows a simple workflow: + + 1. User signs up, inactive account is created. + + 2. Email is sent to user with activation link. + + 3. User clicks activation link, account is now active. + + Using this backend requires that + + * ``registration`` be listed in the ``INSTALLED_APPS`` setting + (since this backend makes use of models defined in this + application). + + * The setting ``ACCOUNT_ACTIVATION_DAYS`` be supplied, specifying + (as an integer) the number of days from registration during + which a user may activate their account (after that period + expires, activation will be disallowed). + + * The creation of the templates + ``registration/activation_email_subject.txt`` and + ``registration/activation_email.txt``, which will be used for + the activation email. See the notes for this backends + ``register`` method for details regarding these templates. + + Additionally, registration can be temporarily closed by adding the + setting ``REGISTRATION_OPEN`` and setting it to + ``False``. Omitting this setting, or setting it to ``True``, will + be interpreted as meaning that registration is currently open and + permitted. + + Internally, this is accomplished via storing an activation key in + an instance of ``registration.models.RegistrationProfile``. See + that model and its custom manager for full documentation of its + fields and supported operations. + + """ + + form_class = RegistrationFormUniqueEmail + + def register(self, request, **cleaned_data): + """ + Given a username, email address and password, register a new + user account, which will initially be inactive. + + Along with the new ``User`` object, a new + ``registration.models.RegistrationProfile`` will be created, + tied to that ``User``, containing the activation key which + will be used for this account. + + An email will be sent to the supplied email address; this + email should contain an activation link. The email will be + rendered using two templates. See the documentation for + ``RegistrationProfile.send_activation_email()`` for + information about these templates and the contexts provided to + them. + + After the ``User`` and ``RegistrationProfile`` are created and + the activation email is sent, the signal + ``registration.signals.user_registered`` will be sent, with + the new ``User`` as the keyword argument ``user`` and the + class of this backend as the sender. + + """ + username, email, password = cleaned_data['username'], cleaned_data['email'], cleaned_data['password1'] + if Site._meta.installed: + site = Site.objects.get_current() + else: + site = RequestSite(request) + new_user = RegistrationProfile.objects.create_inactive_user(username, email, + password, site) + signals.user_registered.send(sender=self.__class__, + user=new_user, + request=request) + return new_user + + def registration_allowed(self, request): + """ + Indicate whether account registration is currently permitted, + based on the value of the setting ``REGISTRATION_OPEN``. This + is determined as follows: + + * If ``REGISTRATION_OPEN`` is not specified in settings, or is + set to ``True``, registration is permitted. + + * If ``REGISTRATION_OPEN`` is both specified and set to + ``False``, registration is not permitted. + + """ + return getattr(settings, 'REGISTRATION_OPEN', True) + + def get_success_url(self, request, user): + """ + Return the name of the URL to redirect to after successful + user registration. + + """ + return ('registration_complete', (), {}) + + +class ActivationView(BaseActivationView): + def activate(self, request, activation_key): + """ + Given an an activation key, look up and activate the user + account corresponding to that key (if possible). + + After successful activation, the signal + ``registration.signals.user_activated`` will be sent, with the + newly activated ``User`` as the keyword argument ``user`` and + the class of this backend as the sender. + + """ + activated_user = RegistrationProfile.objects.activate_user(activation_key) + if activated_user: + signals.user_activated.send(sender=self.__class__, + user=activated_user, + request=request) + return activated_user + + def get_success_url(self, request, user): + return ('registration_activation_complete', (), {}) diff --git a/registration/backends/simple/__init__.py b/registration/backends/simple/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/backends/simple/urls.py b/registration/backends/simple/urls.py new file mode 100644 index 0000000..0be6ee2 --- /dev/null +++ b/registration/backends/simple/urls.py @@ -0,0 +1,36 @@ +""" +URLconf for registration and activation, using django-registration's +one-step backend. + +If the default behavior of these 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.backends.simple.urls')), + +This will also automatically set up the views in +``django.contrib.auth`` at sensible default locations. + +If you'd like to customize registration behavior, feel free to set up +your own URL patterns for these views instead. + +""" + + +from django.conf.urls import include +from django.conf.urls import patterns +from django.conf.urls import url +from django.views.generic.base import TemplateView + +from registration.backends.simple.views import RegistrationView + + +urlpatterns = patterns('', + url(r'^register/$', + RegistrationView.as_view(), + name='registration_register'), + url(r'^register/closed/$', + TemplateView.as_view(template_name='registration/registration_closed.html'), + name='registration_disallowed'), + (r'', include('registration.auth_urls')), + ) diff --git a/registration/backends/simple/views.py b/registration/backends/simple/views.py new file mode 100644 index 0000000..0c85172 --- /dev/null +++ b/registration/backends/simple/views.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.contrib.auth import authenticate +from django.contrib.auth import login +from django.contrib.auth.models import User + +from registration import signals +from registration.views import RegistrationView as BaseRegistrationView + + +class RegistrationView(BaseRegistrationView): + """ + A registration backend which implements the simplest possible + workflow: a user supplies a username, email address and password + (the bare minimum for a useful account), and is immediately signed + up and logged in). + + """ + def register(self, request, **cleaned_data): + username, email, password = cleaned_data['username'], cleaned_data['email'], cleaned_data['password1'] + User.objects.create_user(username, email, password) + + new_user = authenticate(username=username, password=password) + login(request, new_user) + signals.user_registered.send(sender=self.__class__, + user=new_user, + request=request) + return new_user + + def registration_allowed(self, request): + """ + Indicate whether account registration is currently permitted, + based on the value of the setting ``REGISTRATION_OPEN``. This + is determined as follows: + + * If ``REGISTRATION_OPEN`` is not specified in settings, or is + set to ``True``, registration is permitted. + + * If ``REGISTRATION_OPEN`` is both specified and set to + ``False``, registration is not permitted. + + """ + return getattr(settings, 'REGISTRATION_OPEN', True) + + def get_success_url(self, request, user): + return (user.get_absolute_url(), (), {}) diff --git a/registration/forms.py b/registration/forms.py new file mode 100644 index 0000000..de1e92b --- /dev/null +++ b/registration/forms.py @@ -0,0 +1,120 @@ +""" +Forms and validation code for user registration. + +Note that all of these forms assume Django's bundle default ``User`` +model; since it's not possible for a form to anticipate in advance the +needs of custom user models, you will need to write your own forms if +you're using a custom model. + +""" + + +from django.contrib.auth.models import User +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +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 avoid defining a ``save()`` method -- the actual + saving of collected user data is delegated to the active + registration backend. + + """ + required_css_class = 'required' + + username = forms.RegexField(regex=r'^[\w.@+-]+$', + max_length=30, + label=_("Username"), + error_messages={'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")}) + email = forms.EmailField(label=_("E-mail")) + password1 = forms.CharField(widget=forms.PasswordInput, + label=_("Password")) + password2 = forms.CharField(widget=forms.PasswordInput, + label=_("Password (again)")) + + def clean_username(self): + """ + Validate that the username is alphanumeric and is not already + in use. + + """ + existing = User.objects.filter(username__iexact=self.cleaned_data['username']) + if existing.exists(): + raise forms.ValidationError(_("A user with that username already exists.")) + else: + return self.cleaned_data['username'] + + 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(_("The two password fields didn't match.")) + return self.cleaned_data + + +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, + label=_(u'I have read and agree to the Terms of Service'), + error_messages={'required': _("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(_("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', + 'yahoo.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(_("Registration using free email addresses is prohibited. Please supply a different email address.")) + return self.cleaned_data['email'] diff --git a/registration/management/__init__.py b/registration/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/management/commands/__init__.py b/registration/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/management/commands/cleanupregistration.py b/registration/management/commands/cleanupregistration.py new file mode 100644 index 0000000..abec5ae --- /dev/null +++ b/registration/management/commands/cleanupregistration.py @@ -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 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() diff --git a/registration/models.py b/registration/models.py new file mode 100644 index 0000000..2148749 --- /dev/null +++ b/registration/models.py @@ -0,0 +1,271 @@ +import datetime +import hashlib +import random +import re + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from django.db import transaction +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + +try: + from django.contrib.auth import get_user_model + User = get_user_model() +except ImportError: + from django.contrib.auth.models import User + +try: + from django.utils.timezone import now as datetime_now +except ImportError: + datetime_now = datetime.datetime.now + + +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 constant ``RegistrationProfile.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, email, password, + site, send_email=True): + """ + Create a new, inactive ``User``, generate a + ``RegistrationProfile`` and email its activation key to the + ``User``, returning the new ``User``. + + By default, an activation email will be sent to the new + user. To disable this, pass ``send_email=False``. + + """ + 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 send_email: + registration_profile.send_activation_email(site) + + return new_user + create_inactive_user = transaction.commit_on_success(create_inactive_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 = hashlib.sha1(str(random.random())).hexdigest()[:5] + username = user.username + if isinstance(username, unicode): + username = username.encode('utf-8') + activation_key = hashlib.sha1(salt+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(): + try: + if profile.activation_key_expired(): + user = profile.user + if not user.is_active: + user.delete() + profile.delete() + except User.DoesNotExist: + profile.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. + + """ + 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 constant ``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_now()) + activation_key_expired.boolean = True + + def send_activation_email(self, site): + """ + Send an activation email to the user associated with this + ``RegistrationProfile``. + + 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. 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. + + These templates will each receive the following context + variables: + + ``activation_key`` + The activation key for the new account. + + ``expiration_days`` + The number of days remaining during which the account may + be activated. + + ``site`` + An object representing the site on which the user + registered; depending on whether ``django.contrib.sites`` + is installed, this may be an instance of either + ``django.contrib.sites.models.Site`` (if the sites + application is installed) or + ``django.contrib.sites.models.RequestSite`` (if + not). Consult the documentation for the Django sites + framework for details regarding these objects' interfaces. + + """ + ctx_dict = {'activation_key': self.activation_key, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS, + 'site': site} + subject = render_to_string('registration/activation_email_subject.txt', + ctx_dict) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + + message = render_to_string('registration/activation_email.txt', + ctx_dict) + + self.user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) + diff --git a/registration/signals.py b/registration/signals.py new file mode 100644 index 0000000..343e3a5 --- /dev/null +++ b/registration/signals.py @@ -0,0 +1,8 @@ +from django.dispatch import Signal + + +# A new user has registered. +user_registered = Signal(providing_args=["user", "request"]) + +# A user has activated his or her account. +user_activated = Signal(providing_args=["user", "request"]) diff --git a/registration/tests/__init__.py b/registration/tests/__init__.py new file mode 100644 index 0000000..82202ca --- /dev/null +++ b/registration/tests/__init__.py @@ -0,0 +1,4 @@ +from registration.tests.default_backend import * +from registration.tests.forms import * +from registration.tests.models import * +from registration.tests.simple_backend import * diff --git a/registration/tests/default_backend.py b/registration/tests/default_backend.py new file mode 100644 index 0000000..cdf1b0a --- /dev/null +++ b/registration/tests/default_backend.py @@ -0,0 +1,198 @@ +import datetime + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core import mail +from django.core.urlresolvers import reverse +from django.test import TestCase + +from registration import signals +from registration.admin import RegistrationAdmin +from registration.forms import RegistrationForm +from registration.backends.default.views import RegistrationView +from registration.models import RegistrationProfile + + +class DefaultBackendViewTests(TestCase): + """ + Test the default registration backend. + + Running these tests successfully will require two templates to be + created for the sending of activation emails; details on these + templates and their contexts may be found in the documentation for + the default backend. + + """ + urls = 'registration.backends.default.urls' + + def setUp(self): + """ + Create an instance of the default backend for use in testing, + and set ``ACCOUNT_ACTIVATION_DAYS`` if it's not set already. + + """ + self.old_activation = getattr(settings, 'ACCOUNT_ACTIVATION_DAYS', None) + if self.old_activation is None: + settings.ACCOUNT_ACTIVATION_DAYS = 7 # pragma: no cover + + def tearDown(self): + """ + Yank ``ACCOUNT_ACTIVATION_DAYS`` back out if it wasn't + originally set. + + """ + if self.old_activation is None: + settings.ACCOUNT_ACTIVATION_DAYS = self.old_activation # pragma: no cover + + def test_allow(self): + """ + The setting ``REGISTRATION_OPEN`` appropriately controls + whether registration is permitted. + + """ + old_allowed = getattr(settings, 'REGISTRATION_OPEN', True) + settings.REGISTRATION_OPEN = True + + resp = self.client.get(reverse('registration_register')) + self.assertEqual(200, resp.status_code) + + settings.REGISTRATION_OPEN = False + + # Now all attempts to hit the register view should redirect to + # the 'registration is closed' message. + resp = self.client.get(reverse('registration_register')) + self.assertRedirects(resp, reverse('registration_disallowed')) + + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + self.assertRedirects(resp, reverse('registration_disallowed')) + + settings.REGISTRATION_OPEN = old_allowed + + def test_registration_get(self): + """ + HTTP ``GET`` to the registration view uses the appropriate + template and populates a registration form into the context. + + """ + resp = self.client.get(reverse('registration_register')) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, + 'registration/registration_form.html') + self.failUnless(isinstance(resp.context['form'], + RegistrationForm)) + + def test_registration(self): + """ + Registration creates a new inactive account and a new profile + with activation key, populates the correct account data and + sends an activation email. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + self.assertRedirects(resp, reverse('registration_complete')) + + new_user = User.objects.get(username='bob') + + self.failUnless(new_user.check_password('secret')) + self.assertEqual(new_user.email, 'bob@example.com') + + # New user must not be active. + self.failIf(new_user.is_active) + + # A registration profile was created, and an activation email + # was sent. + self.assertEqual(RegistrationProfile.objects.count(), 1) + self.assertEqual(len(mail.outbox), 1) + + def test_registration_no_sites(self): + """ + Registration still functions properly when + ``django.contrib.sites`` is not installed; the fallback will + be a ``RequestSite`` instance. + + """ + Site._meta.installed = False + + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + self.assertEqual(302, resp.status_code) + + new_user = User.objects.get(username='bob') + + self.failUnless(new_user.check_password('secret')) + self.assertEqual(new_user.email, 'bob@example.com') + + self.failIf(new_user.is_active) + + self.assertEqual(RegistrationProfile.objects.count(), 1) + self.assertEqual(len(mail.outbox), 1) + + Site._meta.installed = True + + def test_registration_failure(self): + """ + Registering with invalid data fails. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'notsecret'}) + self.assertEqual(200, resp.status_code) + self.failIf(resp.context['form'].is_valid()) + self.assertEqual(0, len(mail.outbox)) + + def test_activation(self): + """ + Activation of an account functions properly. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + + profile = RegistrationProfile.objects.get(user__username='bob') + + resp = self.client.get(reverse('registration_activate', + args=(), + kwargs={'activation_key': profile.activation_key})) + self.assertRedirects(resp, reverse('registration_activation_complete')) + + def test_activation_expired(self): + """ + An expired account can't be activated. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + + profile = RegistrationProfile.objects.get(user__username='bob') + user = profile.user + user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) + user.save() + + resp = self.client.get(reverse('registration_activate', + args=(), + kwargs={'activation_key': profile.activation_key})) + + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, 'registration/activate.html') + self.failIf('activation_key' in resp.context) diff --git a/registration/tests/forms.py b/registration/tests/forms.py new file mode 100644 index 0000000..dbb1f06 --- /dev/null +++ b/registration/tests/forms.py @@ -0,0 +1,119 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from registration import forms + + +class RegistrationFormTests(TestCase): + """ + Test the default registration forms. + + """ + def test_registration_form(self): + """ + Test that ``RegistrationForm`` enforces username constraints + and matching passwords. + + """ + # Create a user so we can verify that duplicate usernames aren't + # permitted. + User.objects.create_user('alice', 'alice@example.com', 'secret') + + invalid_data_dicts = [ + # Non-alphanumeric username. + {'data': {'username': 'foo/bar', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo'}, + 'error': ('username', [u"This value may contain only letters, numbers and @/./+/-/_ characters."])}, + # Already-existing username. + {'data': {'username': 'alice', + 'email': 'alice@example.com', + 'password1': 'secret', + 'password2': 'secret'}, + 'error': ('username', [u"A user with that username already exists."])}, + # Mismatched passwords. + {'data': {'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'bar'}, + 'error': ('__all__', [u"The two password fields didn't match."])}, + ] + + 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. + + """ + # Create a user so we can verify that duplicate addresses + # aren't permitted. + User.objects.create_user('alice', 'alice@example.com', 'secret') + + 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 forms.RegistrationFormNoFreeEmail.bad_domains: + 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()) diff --git a/registration/tests/models.py b/registration/tests/models.py new file mode 100644 index 0000000..f763cb2 --- /dev/null +++ b/registration/tests/models.py @@ -0,0 +1,225 @@ +import datetime +import re + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core import mail +from django.core import management +from django.test import TestCase +from django.utils.hashcompat import sha_constructor + +from registration.models import RegistrationProfile + + +class RegistrationModelTests(TestCase): + """ + Test the model and manager used in the default backend. + + """ + user_info = {'username': 'alice', + 'password': 'swordfish', + 'email': 'alice@example.com'} + + def setUp(self): + self.old_activation = getattr(settings, 'ACCOUNT_ACTIVATION_DAYS', None) + settings.ACCOUNT_ACTIVATION_DAYS = 7 + + def tearDown(self): + settings.ACCOUNT_ACTIVATION_DAYS = self.old_activation + + def test_profile_creation(self): + """ + Creating a registration profile for a user populates the + profile with the correct user and a SHA1 hash to use as + activation key. + + """ + new_user = User.objects.create_user(**self.user_info) + profile = RegistrationProfile.objects.create_profile(new_user) + + self.assertEqual(RegistrationProfile.objects.count(), 1) + self.assertEqual(profile.user.id, new_user.id) + self.failUnless(re.match('^[a-f0-9]{40}$', profile.activation_key)) + self.assertEqual(unicode(profile), + "Registration information for alice") + + def test_activation_email(self): + """ + ``RegistrationProfile.send_activation_email`` sends an + email. + + """ + new_user = User.objects.create_user(**self.user_info) + profile = RegistrationProfile.objects.create_profile(new_user) + profile.send_activation_email(Site.objects.get_current()) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, [self.user_info['email']]) + + def test_user_creation(self): + """ + Creating a new user populates the correct data, and sets the + user's account inactive. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + self.assertEqual(new_user.username, 'alice') + self.assertEqual(new_user.email, 'alice@example.com') + self.failUnless(new_user.check_password('swordfish')) + self.failIf(new_user.is_active) + + def test_user_creation_email(self): + """ + By default, creating a new user sends an activation email. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + self.assertEqual(len(mail.outbox), 1) + + def test_user_creation_no_email(self): + """ + Passing ``send_email=False`` when creating a new user will not + send an activation email. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + send_email=False, + **self.user_info) + self.assertEqual(len(mail.outbox), 0) + + def test_unexpired_account(self): + """ + ``RegistrationProfile.activation_key_expired()`` is ``False`` + within the activation window. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + profile = RegistrationProfile.objects.get(user=new_user) + self.failIf(profile.activation_key_expired()) + + def test_expired_account(self): + """ + ``RegistrationProfile.activation_key_expired()`` is ``True`` + outside the activation window. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + new_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1) + new_user.save() + profile = RegistrationProfile.objects.get(user=new_user) + self.failUnless(profile.activation_key_expired()) + + def test_valid_activation(self): + """ + Activating a user within the permitted window makes the + account active, and resets the activation key. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + profile = RegistrationProfile.objects.get(user=new_user) + activated = RegistrationProfile.objects.activate_user(profile.activation_key) + + self.failUnless(isinstance(activated, User)) + self.assertEqual(activated.id, new_user.id) + self.failUnless(activated.is_active) + + profile = RegistrationProfile.objects.get(user=new_user) + self.assertEqual(profile.activation_key, RegistrationProfile.ACTIVATED) + + def test_expired_activation(self): + """ + Attempting to activate outside the permitted window does not + activate the account. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + new_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1) + new_user.save() + + profile = RegistrationProfile.objects.get(user=new_user) + activated = RegistrationProfile.objects.activate_user(profile.activation_key) + + self.failIf(isinstance(activated, User)) + self.failIf(activated) + + new_user = User.objects.get(username='alice') + self.failIf(new_user.is_active) + + profile = RegistrationProfile.objects.get(user=new_user) + self.assertNotEqual(profile.activation_key, RegistrationProfile.ACTIVATED) + + def test_activation_invalid_key(self): + """ + Attempting to activate with a key which is not a SHA1 hash + fails. + + """ + self.failIf(RegistrationProfile.objects.activate_user('foo')) + + def test_activation_already_activated(self): + """ + Attempting to re-activate an already-activated account fails. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + profile = RegistrationProfile.objects.get(user=new_user) + RegistrationProfile.objects.activate_user(profile.activation_key) + + profile = RegistrationProfile.objects.get(user=new_user) + self.failIf(RegistrationProfile.objects.activate_user(profile.activation_key)) + + def test_activation_nonexistent_key(self): + """ + Attempting to activate with a non-existent key (i.e., one not + associated with any account) fails. + + """ + # Due to the way activation keys are constructed during + # registration, this will never be a valid key. + invalid_key = sha_constructor('foo').hexdigest() + self.failIf(RegistrationProfile.objects.activate_user(invalid_key)) + + def test_expired_user_deletion(self): + """ + ``RegistrationProfile.objects.delete_expired_users()`` only + deletes inactive users whose activation window has expired. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + expired_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + username='bob', + password='secret', + email='bob@example.com') + expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1) + expired_user.save() + + RegistrationProfile.objects.delete_expired_users() + self.assertEqual(RegistrationProfile.objects.count(), 1) + self.assertRaises(User.DoesNotExist, User.objects.get, username='bob') + + def test_management_command(self): + """ + The ``cleanupregistration`` management command properly + deletes expired accounts. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + **self.user_info) + expired_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(), + username='bob', + password='secret', + email='bob@example.com') + expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1) + expired_user.save() + + management.call_command('cleanupregistration') + self.assertEqual(RegistrationProfile.objects.count(), 1) + self.assertRaises(User.DoesNotExist, User.objects.get, username='bob') diff --git a/registration/tests/simple_backend.py b/registration/tests/simple_backend.py new file mode 100644 index 0000000..fe61079 --- /dev/null +++ b/registration/tests/simple_backend.py @@ -0,0 +1,89 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase + +from registration.forms import RegistrationForm + + +class SimpleBackendViewTests(TestCase): + urls = 'registration.backends.simple.urls' + + def test_allow(self): + """ + The setting ``REGISTRATION_OPEN`` appropriately controls + whether registration is permitted. + + """ + old_allowed = getattr(settings, 'REGISTRATION_OPEN', True) + settings.REGISTRATION_OPEN = True + + resp = self.client.get(reverse('registration_register')) + self.assertEqual(200, resp.status_code) + + settings.REGISTRATION_OPEN = False + + # Now all attempts to hit the register view should redirect to + # the 'registration is closed' message. + resp = self.client.get(reverse('registration_register')) + self.assertRedirects(resp, reverse('registration_disallowed')) + + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + self.assertRedirects(resp, reverse('registration_disallowed')) + + settings.REGISTRATION_OPEN = old_allowed + + def test_registration_get(self): + """ + HTTP ``GET`` to the registration view uses the appropriate + template and populates a registration form into the context. + + """ + resp = self.client.get(reverse('registration_register')) + self.assertEqual(200, resp.status_code) + self.assertTemplateUsed(resp, + 'registration/registration_form.html') + self.failUnless(isinstance(resp.context['form'], + RegistrationForm)) + + def test_registration(self): + """ + Registration creates a new account and logs the user in. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'secret'}) + + new_user = User.objects.get(username='bob') + self.assertEqual(302, resp.status_code) + self.failUnless(new_user.get_absolute_url() in resp['Location']) + + self.failUnless(new_user.check_password('secret')) + self.assertEqual(new_user.email, 'bob@example.com') + + # New user must be active. + self.failUnless(new_user.is_active) + + # New user must be logged in. + resp = self.client.get(reverse('registration_register')) + self.failUnless(resp.context['user'].is_authenticated()) + + def test_registration_failure(self): + """ + Registering with invalid data fails. + + """ + resp = self.client.post(reverse('registration_register'), + data={'username': 'bob', + 'email': 'bob@example.com', + 'password1': 'secret', + 'password2': 'notsecret'}) + self.assertEqual(200, resp.status_code) + self.failIf(resp.context['form'].is_valid()) diff --git a/registration/tests/urls.py b/registration/tests/urls.py new file mode 100644 index 0000000..02ef609 --- /dev/null +++ b/registration/tests/urls.py @@ -0,0 +1,82 @@ +""" +URLs used in the unit tests for django-registration. + +You should not attempt to use these URLs in any sort of real or +development environment; instead, use +``registration/backends/default/urls.py``. This URLconf includes those +URLs, and also adds several additional URLs which serve no purpose +other than to test that optional keyword arguments are properly +handled. + +""" + +from django.conf.urls.defaults import * +from django.views.generic.simple import direct_to_template + +from registration.views import activate +from registration.views import register + + +urlpatterns = patterns('', + # Test the 'activate' view with custom template + # name. + url(r'^activate-with-template-name/(?P\w+)/$', + activate, + {'template_name': 'registration/test_template_name.html', + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_activate_template_name'), + # Test the 'activate' view with + # extra_context_argument. + url(r'^activate-extra-context/(?P\w+)/$', + activate, + {'extra_context': {'foo': 'bar', 'callable': lambda: 'called'}, + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_activate_extra_context'), + # Test the 'activate' view with success_url argument. + url(r'^activate-with-success-url/(?P\w+)/$', + activate, + {'success_url': 'registration_test_custom_success_url', + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_activate_success_url'), + # Test the 'register' view with custom template + # name. + url(r'^register-with-template-name/$', + register, + {'template_name': 'registration/test_template_name.html', + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_register_template_name'), + # Test the'register' view with extra_context + # argument. + url(r'^register-extra-context/$', + register, + {'extra_context': {'foo': 'bar', 'callable': lambda: 'called'}, + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_register_extra_context'), + # Test the 'register' view with custom URL for + # closed registration. + url(r'^register-with-disallowed-url/$', + register, + {'disallowed_url': 'registration_test_custom_disallowed', + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_register_disallowed_url'), + # Set up a pattern which will correspond to the + # custom 'disallowed_url' above. + url(r'^custom-disallowed/$', + direct_to_template, + {'template': 'registration/registration_closed.html'}, + name='registration_test_custom_disallowed'), + # Test the 'register' view with custom redirect + # on successful registration. + url(r'^register-with-success_url/$', + register, + {'success_url': 'registration_test_custom_success_url', + 'backend': 'registration.backends.default.DefaultBackend'}, + name='registration_test_register_success_url' + ), + # Pattern for custom redirect set above. + url(r'^custom-success/$', + direct_to_template, + {'template': 'registration/test_template_name.html'}, + name='registration_test_custom_success_url'), + (r'', include('registration.backends.default.urls')), + ) diff --git a/registration/urls.py b/registration/urls.py new file mode 100644 index 0000000..8579f75 --- /dev/null +++ b/registration/urls.py @@ -0,0 +1,15 @@ +""" +Backwards-compatible URLconf for existing django-registration +installs; this allows the standard ``include('registration.urls')`` to +continue working, but that usage is deprecated and will be removed for +django-registration 1.0. For new installs, use +``include('registration.backends.default.urls')``. + +""" + +import warnings + +warnings.warn("include('registration.urls') is deprecated; use include('registration.backends.default.urls') instead.", + DeprecationWarning) + +from registration.backends.default.urls import * diff --git a/registration/views.py b/registration/views.py new file mode 100644 index 0000000..77aeae1 --- /dev/null +++ b/registration/views.py @@ -0,0 +1,142 @@ +""" +Views which allow users to create and activate accounts. + +""" + +from django.shortcuts import redirect +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView + +from registration import signals +from registration.forms import RegistrationForm + + +class _RequestPassingFormView(FormView): + """ + A version of FormView which passes extra arguments to certain + methods, notably passing the HTTP request nearly everywhere, to + enable finer-grained processing. + + """ + def get(self, request, *args, **kwargs): + # Pass request to get_form_class and get_form for per-request + # form control. + form_class = self.get_form_class(request) + form = self.get_form(form_class) + return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + # Pass request to get_form_class and get_form for per-request + # form control. + form_class = self.get_form_class(request) + form = self.get_form(form_class) + if form.is_valid(): + # Pass request to form_valid. + return self.form_valid(request, form) + else: + return self.form_invalid(form) + + def get_form_class(self, request=None): + return super(_RequestPassingFormView, self).get_form_class() + + def get_form_kwargs(self, request=None, form_class=None): + return super(_RequestPassingFormView, self).get_form_kwargs() + + def get_initial(self, request=None): + return super(_RequestPassingFormView, self).get_initial() + + def get_success_url(self, request=None, user=None): + # We need to be able to use the request and the new user when + # constructing success_url. + return super(_RequestPassingFormView, self).get_success_url() + + def form_valid(self, form, request=None): + return super(_RequestPassingFormView, self).form_valid(form) + + def form_invalid(self, form, request=None): + return super(_RequestPassingFormView, self).form_invalid(form) + + +class RegistrationView(_RequestPassingFormView): + """ + Base class for user registration views. + + """ + disallowed_url = 'registration_disallowed' + form_class = RegistrationForm + http_method_names = ['get', 'post', 'head', 'options', 'trace'] + success_url = None + template_name = 'registration/registration_form.html' + + def dispatch(self, request, *args, **kwargs): + """ + Check that user signup is allowed before even bothering to + dispatch or do other processing. + + """ + if not self.registration_allowed(request): + return redirect(self.disallowed_url) + return super(RegistrationView, self).dispatch(request, *args, **kwargs) + + def form_valid(self, request, form): + new_user = self.register(request, **form.cleaned_data) + success_url = self.get_success_url(request, new_user) + + # success_url may be a simple string, or a tuple providing the + # full argument set for redirect(). Attempting to unpack it + # tells us which one it is. + try: + to, args, kwargs = success_url + return redirect(to, *args, **kwargs) + except ValueError: + return redirect(success_url) + + def registration_allowed(self, request): + """ + Override this to enable/disable user registration, either + globally or on a per-request basis. + + """ + return True + + def register(self, request, **cleaned_data): + """ + Implement user-registration logic here. Access to both the + request and the full cleaned_data of the registration form is + available here. + + """ + raise NotImplementedError + + +class ActivationView(TemplateView): + """ + Base class for user activation views. + + """ + http_method_names = ['get'] + template_name = 'registration/activate.html' + + def get(self, request, *args, **kwargs): + activated_user = self.activate(request, *args, **kwargs) + if activated_user: + signals.user_activated.send(sender=self.__class__, + user=activated_user, + request=request) + success_url = self.get_success_url(request, activated_user) + try: + to, args, kwargs = success_url + return redirect(to, *args, **kwargs) + except ValueError: + return redirect(success_url) + return super(ActivationView, self).get(request, *args, **kwargs) + + def activate(self, request, *args, **kwargs): + """ + Implement account-activation logic here. + + """ + raise NotImplementedError + + def get_success_url(self, request, user): + raise NotImplementedError diff --git a/requirements.txt b/requirements.txt index efc215b..56693b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,24 @@ -Django==1.4.5 +Django==1.5.4 +Markdown==2.3.1 MySQL-python==1.2.4 PIL==1.1.7 -South==0.7.6 +South==0.8.2 anyjson==0.3.3 argparse==1.2.1 -distribute==0.6.36 -django-braces==1.0.0 +distribute==0.7.3 +django-braces==1.2.2 django-disqus==0.4.1 -django-extensions==1.0.3 -django-guardian +django-extensions==1.2.0 +django-guardian==1.1.1 +django-haystack==2.1.0 django-ratings==0.3.7 -django-registration==0.8 -django-widget-tweaks==1.1.2 -gunicorn==0.17.2 +django-registration==1.0 +django-widget-tweaks==1.3 +gunicorn==18.0 +ipython==1.0.0 +pyelasticsearch==0.6 python-dateutil==2.1 +requests==2.0.0 +simplejson==3.3.1 +six==1.4.1 wsgiref==0.1.2 -Markdown==2.3.1 -django-haystack==2.0.0 -pyelasticsearch==0.5 -requests==1.2.3 diff --git a/templates/500.html b/templates/500.html index 212ab09..ef92bee 100644 --- a/templates/500.html +++ b/templates/500.html @@ -1,6 +1,80 @@ -{% extends "base.html" %} -{% block head_title %}Server Error(500 error){% endblock %} -{% block content %} -

Server error(500)

-

Sorry, our server resulted some error. We have informed our administrators. Till then try going to Home!

-{% endblock %} + + + + +Server Error(500 error) | Codesters - hub to learn programming + + + + + + + + + + +
+

Server Error(500 error)

+

Sorry, some error occured on our server. We have let our admins know about it. Please try again later.

+
+ + + + + + diff --git a/templates/about.html b/templates/about.html deleted file mode 100644 index 9fd8a84..0000000 --- a/templates/about.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
-

About Us

-

We have actually made this website for us. We want to learn new things related to Computer Science. But what happens everytime is we waste much of our time in finding good resources to learn from.

-

This website aims to become a public library cum recommendation hub for learning. You want to learn programming or any computer science topic. Search it in topics and you are good to go.

- Get Started -
-

How we do it?

-

- We collect content across the web and submit it here. There is no crawler doing this. We hand picked these resources from our bookmarks and recommendations given by many people. -

-

- Then some initial rating is calculated and listed in the Resources Section under the appropriate category and topic. You can view resources according to their popularity and recent-ness. -

-

- Also each Topic has its own page explaining what it is and how it is used in real world. This gives only beginners a view about the topic and they can decide if they want to learn it or not. We are working on making this crowdsourced so that experts in the field can make it even better. -

-

- We are also working on forum(also evaluating hosted option) so that learners can ask questions easily. Even silly questions that get tagged unappropriate in Stack Overflow are welcome. For more Check out Guidelines Section to get a knowledge of functioning and our expectation from you. -

-
-

I am a programmer

-

Share your knowledge, add resources which you think can help any person wanting to learn programming. If you are familier with Django, you can help us develop this project.

- Codesters on Github -
-{% endblock %} diff --git a/templates/base.html b/templates/base.html index 718a9ae..4a1f3f9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,5 +1,4 @@ {% extends "bare_base.html" %} -{% load url from future %} {% block head_title %}{{ headline }}{% endblock head_title %} {% block body %} diff --git a/templates/coming_soon.html b/templates/coming_soon.html deleted file mode 100644 index 80c8746..0000000 --- a/templates/coming_soon.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block head_title %}Coming Soon{% endblock %} - -{% block content %} -
-

We are actively developing this website. Soon you will be able to see this content. Till then browse other features...

-

or Learn more about us and our plans.

-
-{% endblock %} diff --git a/templates/contact.html b/templates/contact.html deleted file mode 100644 index d525b1c..0000000 --- a/templates/contact.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block content %} -
-

Contact Us

-

Have any suggestion or complaint?

-

Mail us at admin@codesters.org or send a tweet on twitter

-

We will get back to you as soon as possible :)

-
-{% endblock %} diff --git a/templates/explore/explore_home.html b/templates/explore/explore_home.html index 5e29fd3..58a08c6 100644 --- a/templates/explore/explore_home.html +++ b/templates/explore/explore_home.html @@ -96,48 +96,4 @@

Popular Domains (Browse all -
-
-

Most Active Users

- - - - - - - - - - {% for user in active_users %} - - - - - - {% endfor %} - -
RankUsernameResources Submitted
{{ forloop.counter }}@{{ user.username }}{{ user.no_of_resources }}
-
-
-

Recent Snippets from Users

- - - - - - - - - - {% for snippet in recent_snippets %} - - - - - - {% endfor %} - -
#TitleUser
{{ forloop.counter }}{{ snippet.title }}{{ snippet.user }}
-
-
{% endblock %} diff --git a/templates/feeds/resources.html b/templates/feeds/resources.html index 576f145..1a6b722 100644 --- a/templates/feeds/resources.html +++ b/templates/feeds/resources.html @@ -1,4 +1,4 @@ -{% load markup %} +{% load markdown_deux_tags %} {% load url from future %} diff --git a/templates/feeds/snippets.html b/templates/feeds/snippets.html index f188856..fcbe0c2 100644 --- a/templates/feeds/snippets.html +++ b/templates/feeds/snippets.html @@ -1,2 +1,2 @@ -{% load markup %} +{% load markdown_deux_tags %} {{ obj.content|markdown:"safe" }} diff --git a/templates/flatpages/default.html b/templates/flatpages/default.html new file mode 100644 index 0000000..5970c96 --- /dev/null +++ b/templates/flatpages/default.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block head_title %}{{ flatpage.title }}{% endblock head_title %} +{% block content %} +
{{ flatpage.content }}
+{% endblock content %} diff --git a/templates/guidelines.html b/templates/guidelines.html deleted file mode 100644 index 0510d9c..0000000 --- a/templates/guidelines.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} -{% load url from future %} - -{% block head_title %}Guidelines{% endblock %} -{% block content %} -

Coming Soon

-{% endblock %} diff --git a/templates/index.html b/templates/index.html index 8fe8ec2..d94707e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load url from future %} {% block content %}
diff --git a/templates/pagination.html b/templates/pagination.html index 8f6b7dc..e7cdeea 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -7,7 +7,7 @@ {% else %} {% endif %}
  • @@ -19,7 +19,7 @@
  • {% else %} {% endif %} diff --git a/profiles/templates/profiles/base.html b/templates/profiles/base.html similarity index 74% rename from profiles/templates/profiles/base.html rename to templates/profiles/base.html index d906b01..fb83a23 100644 --- a/profiles/templates/profiles/base.html +++ b/templates/profiles/base.html @@ -1,14 +1,8 @@ {% extends "base.html" %} -{% load url from future %} {% load active from codesters %} {% load gravatar %} -{% block extra_head %} - - -{% endblock %} - {% block content %}
    @@ -32,9 +26,6 @@ {% if userinfo.userprofile.twitter %}
  • Twitter
  • {% endif %} {% if userinfo.userprofile.stackoverflow %}
  • Stackoverflow
  • {% endif %} {% if userinfo.userprofile.facebook %}
  • Facebook
  • {% endif %} - -
  • Rss
  • -
  • Atom
  • @@ -43,8 +34,6 @@ - {% if user.is_staff %} -
  • Manage
  • - {% endif %} {% else %}
  • Login
  • Register