diff --git a/.travis.yml b/.travis.yml index 5fd39b1..8679266 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: python python: - - "2.6" - "2.7" # command to install dependencies install: - pip install -r travis.txt --use-mirrors - python setup.py develop script: python example/manage.py test follow -v2 - \ No newline at end of file + diff --git a/example/__init__.py b/example/example/__init__.py similarity index 100% rename from example/__init__.py rename to example/example/__init__.py diff --git a/example/example/settings.py b/example/example/settings.py new file mode 100644 index 0000000..9da7a47 --- /dev/null +++ b/example/example/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for example. + +Generated by 'django-admin startproject' using Django 1.9.4. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'b)4j*-=ctt88akc6ojfcl&3%7z7x3@tc85_sc&wji#v$ipnwx+' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'follow' +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'example.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/example/urls.py b/example/example/urls.py similarity index 90% rename from example/urls.py rename to example/example/urls.py index f456f7f..7331c9a 100644 --- a/example/urls.py +++ b/example/example/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, include, url +from django.conf.urls import patterns, include, url # Uncomment the next two lines to enable the admin: # from django.contrib import admin diff --git a/example/example/wsgi.py b/example/example/wsgi.py new file mode 100644 index 0000000..512ee29 --- /dev/null +++ b/example/example/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for example_project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") + +application = get_wsgi_application() diff --git a/example/manage.py b/example/manage.py old mode 100644 new mode 100755 index 3e4eedc..2605e37 --- a/example/manage.py +++ b/example/manage.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -from django.core.management import execute_manager -import imp -try: - imp.find_module('settings') # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) - sys.exit(1) - -import settings +import os +import sys if __name__ == "__main__": - execute_manager(settings) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/example/settings.py b/example/settings.py deleted file mode 100644 index 4879fd1..0000000 --- a/example/settings.py +++ /dev/null @@ -1,146 +0,0 @@ -# Django settings for project project. -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'db.sqlite', # 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. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = '' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '&4hhsd5b_elzi1p3*cd(a-fmlufeal^3^l#v$hmuqv!3$fbh39' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'example.urls' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'app', - 'follow', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} diff --git a/follow/tests.py b/follow/tests.py index d5079ed..19418a1 100644 --- a/follow/tests.py +++ b/follow/tests.py @@ -1,102 +1,105 @@ from django import template from django.contrib.auth.models import User, AnonymousUser, Group from django.core.urlresolvers import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from follow import signals, utils from follow.models import Follow from follow.utils import register +import follow.urls register(User) register(Group) class FollowTest(TestCase): - urls = 'follow.urls' - + @override_settings(ROOT_URLCONF=follow.urls) def setUp(self): - + self.lennon = User.objects.create(username='lennon') self.lennon.set_password('test') self.lennon.save() self.hendrix = User.objects.create(username='hendrix') - + self.musicians = Group.objects.create() - - self.lennon.groups.add(self.musicians) - + + self.lennon.groups.add(self.musicians) + def test_follow(self): follow = Follow.objects.create(self.lennon, self.hendrix) - + _, result = Follow.objects.get_or_create(self.lennon, self.hendrix) self.assertEqual(False, result) - + result = Follow.objects.is_following(self.lennon, self.hendrix) self.assertEqual(True, result) - + result = Follow.objects.is_following(self.hendrix, self.lennon) self.assertEqual(False, result) result = Follow.objects.get_follows(User) self.assertEqual(1, len(result)) self.assertEqual(self.lennon, result[0].user) - + result = Follow.objects.get_follows(self.hendrix) self.assertEqual(1, len(result)) self.assertEqual(self.lennon, result[0].user) - + result = self.hendrix.get_follows() self.assertEqual(1, len(result)) self.assertEqual(self.lennon, result[0].user) - + result = self.lennon.get_follows() self.assertEqual(0, len(result), result) - + utils.toggle(self.lennon, self.hendrix) self.assertEqual(0, len(self.hendrix.get_follows())) - + utils.toggle(self.lennon, self.hendrix) self.assertEqual(1, len(self.hendrix.get_follows())) - + def test_get_follows_for_queryset(self): utils.follow(self.hendrix, self.lennon) utils.follow(self.lennon, self.hendrix) - + result = Follow.objects.get_follows(User.objects.all()) self.assertEqual(2, result.count()) - + def test_follow_http(self): + + User.get_absolute_url = lambda u: "/users/%s/" % u.username + self.client.login(username='lennon', password='test') - + follow_url = reverse('follow', args=['auth', 'user', self.hendrix.id]) unfollow_url = reverse('follow', args=['auth', 'user', self.hendrix.id]) toggle_url = reverse('toggle', args=['auth', 'user', self.hendrix.id]) response = self.client.post(follow_url) self.assertEqual(302, response.status_code) - + response = self.client.post(follow_url) self.assertEqual(302, response.status_code) - + response = self.client.post(unfollow_url) self.assertEqual(302, response.status_code) - + response = self.client.post(toggle_url) self.assertEqual(302, response.status_code) - + def test_get_fail(self): self.client.login(username='lennon', password='test') follow_url = reverse('follow', args=['auth', 'user', self.hendrix.id]) unfollow_url = reverse('follow', args=['auth', 'user', self.hendrix.id]) - + response = self.client.get(follow_url) self.assertEqual(400, response.status_code) - + response = self.client.get(unfollow_url) self.assertEqual(400, response.status_code) - + def test_no_absolute_url(self): self.client.login(username='lennon', password='test') - get_absolute_url = User.get_absolute_url + #get_absolute_url = User.get_absolute_url User.get_absolute_url = None follow_url = utils.follow_link(self.hendrix) @@ -107,48 +110,48 @@ def test_no_absolute_url(self): def test_template_tags(self): follow_url = reverse('follow', args=['auth', 'user', self.hendrix.id]) unfollow_url = reverse('unfollow', args=['auth', 'user', self.hendrix.id]) - + request = type('Request', (object,), {'user': self.lennon})() - + self.assertEqual(follow_url, utils.follow_link(self.hendrix)) self.assertEqual(unfollow_url, utils.unfollow_link(self.hendrix)) - + tpl = template.Template("""{% load follow_tags %}{% follow_url obj %}""") ctx = template.Context({ 'obj':self.hendrix, 'request': request }) - + self.assertEqual(follow_url, tpl.render(ctx)) - + utils.follow(self.lennon, self.hendrix) - + self.assertEqual(unfollow_url, tpl.render(ctx)) - + utils.unfollow(self.lennon, self.hendrix) - + self.assertEqual(follow_url, tpl.render(ctx)) - + tpl = template.Template("""{% load follow_tags %}{% follow_url obj user %}""") ctx2 = template.Context({ 'obj': self.lennon, 'user': self.hendrix, 'request': request }) - + self.assertEqual(utils.follow_url(self.hendrix, self.lennon), tpl.render(ctx2)) - + tpl = template.Template("""{% load follow_tags %}{% if request.user|is_following:obj %}True{% else %}False{% endif %}""") - + self.assertEqual("False", tpl.render(ctx)) - + utils.follow(self.lennon, self.hendrix) - + self.assertEqual("True", tpl.render(ctx)) - + tpl = template.Template("""{% load follow_tags %}{% follow_form obj %}""") self.assertEqual(True, isinstance(tpl.render(ctx), unicode)) - + tpl = template.Template("""{% load follow_tags %}{% follow_form obj "follow/form.html" %}""") self.assertEqual(True, isinstance(tpl.render(ctx), unicode)) @@ -159,47 +162,44 @@ def test_signals(self): }) user_handler = Handler() group_handler = Handler() - + def follow_handler(sender, user, target, instance, **kwargs): self.assertEqual(sender, User) self.assertEqual(self.lennon, user) self.assertEqual(self.hendrix, target) self.assertEqual(True, isinstance(instance, Follow)) user_handler.inc() - + def unfollow_handler(sender, user, target, instance, **kwargs): self.assertEqual(sender, User) self.assertEqual(self.lennon, user) self.assertEqual(self.hendrix, target) self.assertEqual(True, isinstance(instance, Follow)) user_handler.inc() - + def group_follow_handler(sender, **kwargs): self.assertEqual(sender, Group) - group_handler.inc() - + group_handler.inc() + def group_unfollow_handler(sender, **kwargs): self.assertEqual(sender, Group) group_handler.inc() - + signals.followed.connect(follow_handler, sender=User, dispatch_uid='userfollow') signals.unfollowed.connect(unfollow_handler, sender=User, dispatch_uid='userunfollow') - + signals.followed.connect(group_follow_handler, sender=Group, dispatch_uid='groupfollow') signals.unfollowed.connect(group_unfollow_handler, sender=Group, dispatch_uid='groupunfollow') - + utils.follow(self.lennon, self.hendrix) utils.unfollow(self.lennon, self.hendrix) self.assertEqual(2, user_handler.i) - + utils.follow(self.lennon, self.musicians) utils.unfollow(self.lennon, self.musicians) - + self.assertEqual(2, user_handler.i) self.assertEqual(2, group_handler.i) def test_anonymous_is_following(self): self.assertEqual(False, Follow.objects.is_following(AnonymousUser(), self.lennon)) - - - diff --git a/follow/urls.py b/follow/urls.py index fa681d2..ab31dd2 100644 --- a/follow/urls.py +++ b/follow/urls.py @@ -1,7 +1,8 @@ -from django.conf.urls.defaults import * +from django.conf.urls import patterns, url, include -urlpatterns = patterns('', - url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', 'follow.views.toggle', name='toggle'), - url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', 'follow.views.toggle', name='follow'), - url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', 'follow.views.toggle', name='unfollow'), -) +import follow.views +urlpatterns = [ + url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', follow.views.toggle, name='toggle'), + url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', follow.views.toggle, name='follow'), + url(r'^toggle/(?P[^\/]+)/(?P[^\/]+)/(?P\d+)/$', follow.views.toggle, name='unfollow'), +] diff --git a/follow/utils.py b/follow/utils.py index aa0d4b6..618ccb2 100644 --- a/follow/utils.py +++ b/follow/utils.py @@ -9,26 +9,26 @@ def get_followers_for_object(instance): def register(model, field_name=None, related_name=None, lookup_method_name='get_follows'): """ This registers any model class to be follow-able. - + """ if model in registry: return registry.append(model) - + if not field_name: - field_name = 'target_%s' % model._meta.module_name - + field_name = 'target_%s' % model._meta.model_name + if not related_name: - related_name = 'follow_%s' % model._meta.module_name - + related_name = 'follow_%s' % model._meta.model_name + field = ForeignKey(model, related_name=related_name, null=True, blank=True, db_index=True) - + field.contribute_to_class(Follow, field_name) setattr(model, lookup_method_name, get_followers_for_object) model_map[model] = [related_name, field_name] - + def follow(user, obj): """ Make a user follow an object """ follow, created = Follow.objects.get_or_create(user, obj) @@ -39,7 +39,7 @@ def unfollow(user, obj): try: follow = Follow.objects.get_follows(obj).get(user=user) follow.delete() - return follow + return follow except Follow.DoesNotExist: pass @@ -48,19 +48,18 @@ def toggle(user, obj): checks but just toggle it on / off. """ if Follow.objects.is_following(user, obj): return unfollow(user, obj) - return follow(user, obj) + return follow(user, obj) def follow_link(object): - return reverse('follow.views.toggle', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) + return reverse('follow', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) def unfollow_link(object): - return reverse('follow.views.toggle', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) + return reverse('unfollow', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) def toggle_link(object): - return reverse('follow.views.toggle', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) + return reverse('toggle', args=[object._meta.app_label, object._meta.object_name.lower(), object.pk]) def follow_url(user, obj): """ Returns the right follow/unfollow url """ return toggle_link(obj) - diff --git a/follow/views.py b/follow/views.py index 01c6791..ee3372f 100644 --- a/follow/views.py +++ b/follow/views.py @@ -1,11 +1,11 @@ from django.contrib.auth.decorators import login_required -from django.db.models.loading import cache +from django.apps import apps from django.http import HttpResponse, HttpResponseRedirect, \ HttpResponseServerError, HttpResponseBadRequest from follow.utils import follow as _follow, unfollow as _unfollow, toggle as _toggle def check(func): - """ + """ Check the permissions, http method and login state. """ def iCheck(request, *args, **kwargs): @@ -32,14 +32,14 @@ def iCheck(request, *args, **kwargs): @login_required @check def follow(request, app, model, id): - model = cache.get_model(app, model) + model = apps.get_model(app, model) obj = model.objects.get(pk=id) return _follow(request.user, obj) @login_required @check def unfollow(request, app, model, id): - model = cache.get_model(app, model) + model = apps.get_model(app, model) obj = model.objects.get(pk=id) return _unfollow(request.user, obj) @@ -47,6 +47,6 @@ def unfollow(request, app, model, id): @login_required @check def toggle(request, app, model, id): - model = cache.get_model(app, model) + model = apps.get_model(app, model) obj = model.objects.get(pk=id) return _toggle(request.user, obj)