Merge pull request #1376 from edx/adam/disable-accounts-2
Adam/disable accounts 2
This commit is contained in:
@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Common: Adds ability to disable a student's account. Students with disabled
|
||||
accounts will be prohibited from site access.
|
||||
|
||||
LMS: Fix issue with CourseMode expiration dates
|
||||
|
||||
LMS: Ported bulk emailing to the beta instructor dashboard.
|
||||
|
||||
@@ -150,6 +150,7 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
# Instead of AuthenticationMiddleware, we use a cache-backed version
|
||||
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
|
||||
'student.middleware.UserStandingMiddleware',
|
||||
'contentserver.middleware.StaticContentServer',
|
||||
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
|
||||
37
common/djangoapps/student/middleware.py
Normal file
37
common/djangoapps/student/middleware.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Middleware that checks user standing for the purpose of keeping users with
|
||||
disabled accounts from accessing the site.
|
||||
"""
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from student.models import UserStanding
|
||||
|
||||
class UserStandingMiddleware(object):
|
||||
"""
|
||||
Checks a user's standing on request. Returns a 403 if the user's
|
||||
status is 'disabled'.
|
||||
"""
|
||||
def process_request(self, request):
|
||||
user = request.user
|
||||
try:
|
||||
user_account = UserStanding.objects.get(user=user.id)
|
||||
# because user is a unique field in UserStanding, there will either be
|
||||
# one or zero user_accounts associated with a UserStanding
|
||||
except UserStanding.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if user_account.account_status == UserStanding.ACCOUNT_DISABLED:
|
||||
msg = _(
|
||||
'Your account has been disabled. If you believe '
|
||||
'this was done in error, please contact us at '
|
||||
'{link_start}{support_email}{link_end}'
|
||||
).format(
|
||||
support_email=settings.DEFAULT_FEEDBACK_EMAIL,
|
||||
link_start=u'<a href="mailto:{address}?subject={subject_line}">'.format(
|
||||
address=settings.DEFAULT_FEEDBACK_EMAIL,
|
||||
subject_line=_('Disabled Account'),
|
||||
),
|
||||
link_end=u'</a>'
|
||||
)
|
||||
return HttpResponseForbidden(msg)
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- 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):
|
||||
# Adding model 'UserStanding'
|
||||
db.create_table('student_userstanding', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='standing', unique=True, to=orm['auth.User'])),
|
||||
('account_status', self.gf('django.db.models.fields.CharField')(max_length=31, blank=True)),
|
||||
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], blank=True)),
|
||||
('standing_last_changed_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['UserStanding'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'UserStanding'
|
||||
db.delete_table('student_userstanding')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'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': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'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'}),
|
||||
'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'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.courseenrollmentallowed': {
|
||||
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
|
||||
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.testcenterregistration': {
|
||||
'Meta': {'object_name': 'TestCenterRegistration'},
|
||||
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'accommodation_request': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
|
||||
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
|
||||
'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
|
||||
'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
|
||||
},
|
||||
'student.testcenteruser': {
|
||||
'Meta': {'object_name': 'TestCenterUser'},
|
||||
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
|
||||
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
|
||||
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
|
||||
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
|
||||
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
|
||||
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
|
||||
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
|
||||
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
|
||||
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
|
||||
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
|
||||
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
|
||||
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
|
||||
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.userstanding': {
|
||||
'Meta': {'object_name': 'UserStanding'},
|
||||
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -33,6 +33,27 @@ from pytz import UTC
|
||||
log = logging.getLogger(__name__)
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
class UserStanding(models.Model):
|
||||
"""
|
||||
This table contains a student's account's status.
|
||||
Currently, we're only disabling accounts; in the future we can imagine
|
||||
taking away more specific privileges, like forums access, or adding
|
||||
more specific karma levels or probationary stages.
|
||||
"""
|
||||
ACCOUNT_DISABLED = "disabled"
|
||||
ACCOUNT_ENABLED = "enabled"
|
||||
USER_STANDING_CHOICES = (
|
||||
(ACCOUNT_DISABLED, u"Account Disabled"),
|
||||
(ACCOUNT_ENABLED, u"Account Enabled"),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(User, db_index=True, related_name='standing', unique=True)
|
||||
account_status = models.CharField(
|
||||
blank=True, max_length=31, choices=USER_STANDING_CHOICES
|
||||
)
|
||||
changed_by = models.ForeignKey(User, blank=True)
|
||||
standing_last_changed_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
"""This is where we store all the user demographic fields. We have a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment,
|
||||
PendingEmailChange)
|
||||
PendingEmailChange, UserStanding,
|
||||
)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
|
||||
@@ -16,6 +17,13 @@ class GroupFactory(DjangoModelFactory):
|
||||
|
||||
name = u'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
class UserStandingFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = UserStanding
|
||||
|
||||
user = None
|
||||
account_status = None
|
||||
changed_by = None
|
||||
|
||||
|
||||
class UserProfileFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
115
common/djangoapps/student/tests/test_userstanding.py
Normal file
115
common/djangoapps/student/tests/test_userstanding.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
These are tests for disabling and enabling student accounts, and for making sure
|
||||
that students with disabled accounts are unable to access the courseware.
|
||||
"""
|
||||
from student.tests.factories import UserFactory, UserStandingFactory
|
||||
from student.models import UserStanding
|
||||
from django.test import TestCase, Client
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class UserStandingTest(TestCase):
|
||||
"""test suite for user standing view for enabling and disabling accounts"""
|
||||
|
||||
def setUp(self):
|
||||
# create users
|
||||
self.bad_user = UserFactory.create(
|
||||
username='bad_user',
|
||||
)
|
||||
self.good_user = UserFactory.create(
|
||||
username='good_user',
|
||||
)
|
||||
self.non_staff = UserFactory.create(
|
||||
username='non_staff',
|
||||
)
|
||||
self.admin = UserFactory.create(
|
||||
username='admin',
|
||||
is_staff=True,
|
||||
)
|
||||
|
||||
# create clients
|
||||
self.bad_user_client = Client()
|
||||
self.good_user_client = Client()
|
||||
self.non_staff_client = Client()
|
||||
self.admin_client = Client()
|
||||
|
||||
for user, client in [
|
||||
(self.bad_user, self.bad_user_client),
|
||||
(self.good_user, self.good_user_client),
|
||||
(self.non_staff, self.non_staff_client),
|
||||
(self.admin, self.admin_client),
|
||||
]:
|
||||
client.login(username=user.username, password='test')
|
||||
|
||||
UserStandingFactory.create(
|
||||
user=self.bad_user,
|
||||
account_status=UserStanding.ACCOUNT_DISABLED,
|
||||
changed_by=self.admin
|
||||
)
|
||||
|
||||
# set different stock urls for lms and cms
|
||||
# to test disabled accounts' access to site
|
||||
try:
|
||||
self.some_url = reverse('dashboard')
|
||||
except NoReverseMatch:
|
||||
self.some_url = reverse('index')
|
||||
|
||||
# since it's only possible to disable accounts from lms, we're going
|
||||
# to skip tests for cms
|
||||
|
||||
def test_disable_account(self):
|
||||
self.assertEqual(
|
||||
UserStanding.objects.filter(user=self.good_user).count(), 0
|
||||
)
|
||||
try:
|
||||
response = self.admin_client.post(reverse('disable_account_ajax'), {
|
||||
'username': self.good_user.username,
|
||||
'account_action': 'disable',
|
||||
})
|
||||
except NoReverseMatch:
|
||||
raise SkipTest()
|
||||
self.assertEqual(
|
||||
UserStanding.objects.get(user=self.good_user).account_status,
|
||||
UserStanding.ACCOUNT_DISABLED
|
||||
)
|
||||
|
||||
def test_disabled_account_403s(self):
|
||||
response = self.bad_user_client.get(self.some_url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_reenable_account(self):
|
||||
try:
|
||||
response = self.admin_client.post(reverse('disable_account_ajax'), {
|
||||
'username': self.bad_user.username,
|
||||
'account_action': 'reenable'
|
||||
})
|
||||
except NoReverseMatch:
|
||||
raise SkipTest()
|
||||
self.assertEqual(
|
||||
UserStanding.objects.get(user=self.bad_user).account_status,
|
||||
UserStanding.ACCOUNT_ENABLED
|
||||
)
|
||||
|
||||
def test_non_staff_cant_access_disable_view(self):
|
||||
try:
|
||||
response = self.non_staff_client.get(reverse('manage_user_standing'), {
|
||||
'user': self.non_staff,
|
||||
})
|
||||
except NoReverseMatch:
|
||||
raise SkipTest()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_non_staff_cant_disable_account(self):
|
||||
try:
|
||||
response = self.non_staff_client.post(reverse('disable_account_ajax'), {
|
||||
'username': self.good_user.username,
|
||||
'user': self.non_staff,
|
||||
'account_action': 'disable'
|
||||
})
|
||||
except NoReverseMatch:
|
||||
raise SkipTest()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(
|
||||
UserStanding.objects.filter(user=self.good_user).count(), 0
|
||||
)
|
||||
@@ -16,6 +16,7 @@ from django.contrib.auth import logout, authenticate, login
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.views import password_reset_confirm
|
||||
# from django.contrib.sessions.models import Session
|
||||
from django.core.cache import cache
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
@@ -29,18 +30,21 @@ from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.utils.http import cookie_date, base36_to_int, urlencode
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.utils.translation import ugettext as _u
|
||||
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
|
||||
TestCenterRegistration, TestCenterRegistrationForm,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment, unique_id_for_user,
|
||||
get_testcenter_registration, CourseEnrollmentAllowed)
|
||||
from student.models import (
|
||||
Registration, UserProfile, TestCenterUser, TestCenterUserForm,
|
||||
TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
|
||||
PendingEmailChange, CourseEnrollment, unique_id_for_user,
|
||||
get_testcenter_registration, CourseEnrollmentAllowed, UserStanding,
|
||||
)
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
@@ -65,6 +69,8 @@ import track.views
|
||||
from dogapi import dog_stats_api
|
||||
from pytz import UTC
|
||||
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
@@ -597,6 +603,81 @@ def logout_user(request):
|
||||
domain=settings.SESSION_COOKIE_DOMAIN)
|
||||
return response
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_user_standing(request):
|
||||
"""
|
||||
Renders the view used to manage user standing. Also displays a table
|
||||
of user accounts that have been disabled and who disabled them.
|
||||
"""
|
||||
if not request.user.is_staff:
|
||||
raise Http404
|
||||
all_disabled_accounts = UserStanding.objects.filter(
|
||||
account_status=UserStanding.ACCOUNT_DISABLED
|
||||
)
|
||||
|
||||
all_disabled_users = [standing.user for standing in all_disabled_accounts]
|
||||
|
||||
headers = ['username', 'account_changed_by']
|
||||
rows = []
|
||||
for user in all_disabled_users:
|
||||
row = [user.username, user.standing.all()[0].changed_by]
|
||||
rows.append(row)
|
||||
|
||||
|
||||
context = {'headers': headers, 'rows': rows}
|
||||
|
||||
return render_to_response("manage_user_standing.html", context)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def disable_account_ajax(request):
|
||||
"""
|
||||
Ajax call to change user standing. Endpoint of the form
|
||||
in manage_user_standing.html
|
||||
"""
|
||||
if not request.user.is_staff:
|
||||
raise Http404
|
||||
username = request.POST.get('username')
|
||||
context = {}
|
||||
if username is None or username.strip() == '':
|
||||
context['message'] = _u('Please enter a username')
|
||||
return JsonResponse(context, status=400)
|
||||
|
||||
account_action = request.POST.get('account_action')
|
||||
if account_action is None:
|
||||
context['message'] = _u('Please choose an option')
|
||||
return JsonResponse(context, status=400)
|
||||
|
||||
username = username.strip()
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
context['message'] = _u("User with username {} does not exist").format(username)
|
||||
return JsonResponse(context, status=400)
|
||||
else:
|
||||
user_account, _ = UserStanding.objects.get_or_create(
|
||||
user=user, defaults={'changed_by': request.user},
|
||||
)
|
||||
if account_action == 'disable':
|
||||
user_account.account_status = UserStanding.ACCOUNT_DISABLED
|
||||
context['message'] = _u("Successfully disabled {}'s account").format(username)
|
||||
log.info("{} disabled {}'s account".format(request.user, username))
|
||||
elif account_action == 'reenable':
|
||||
user_account.account_status = UserStanding.ACCOUNT_ENABLED
|
||||
context['message'] = _u("Successfully reenabled {}'s account").format(username)
|
||||
log.info("{} reenabled {}'s account".format(request.user, username))
|
||||
else:
|
||||
context['message'] = _u("Unexpected account status")
|
||||
return JsonResponse(context, status=400)
|
||||
user_account.changed_by = request.user
|
||||
user_account.standing_last_changed_at = datetime.datetime.now(UTC)
|
||||
user_account.save()
|
||||
|
||||
return JsonResponse(context)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -569,6 +569,7 @@ MIDDLEWARE_CLASSES = (
|
||||
# Instead of AuthenticationMiddleware, we use a cached backed version
|
||||
#'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
|
||||
'student.middleware.UserStandingMiddleware',
|
||||
'contentserver.middleware.StaticContentServer',
|
||||
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%inherit file="main.html" />
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
56
lms/templates/manage_user_standing.html
Normal file
56
lms/templates/manage_user_standing.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<%inherit file="main.html" />
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<h2>${_("Disable or Reenable student accounts")}</h2>
|
||||
<form action="${reverse('disable_account_ajax')}" method="post" data-remote="true" id="disable-form">
|
||||
<label for="username">${_("Username:")}</label>
|
||||
<input type="text" id="username" name="username" required="true">
|
||||
<br>
|
||||
<label for="account_action">${_("Disable Account")}</label>
|
||||
<input type="radio" name="account_action" value="disable" id="account_action">
|
||||
<br>
|
||||
<label for="account_action">${_("Reenable Account")}</label>
|
||||
<input type="radio" name="account_action" value="reenable" id="account_action">
|
||||
<br>
|
||||
<br>
|
||||
</form>
|
||||
|
||||
<button id="submit-form">${_("Submit")}</button>
|
||||
<br>
|
||||
<br>
|
||||
<p id="account-change-status"></p>
|
||||
<h2>${_("Students whose accounts have been disabled")}</h2>
|
||||
<p>${_("(reload your page to refresh)")}</p>
|
||||
<table id="account-table" border='1'>
|
||||
<tr>
|
||||
% for header in headers:
|
||||
<th>${header}</th>
|
||||
% endfor
|
||||
</tr>
|
||||
% for row in rows:
|
||||
<tr>
|
||||
% for cell in row:
|
||||
<td>${cell}</td>
|
||||
% endfor
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var form = $("#disable-form");
|
||||
$("#submit-form").click(function(){
|
||||
$("#account-change-status").html(gettext("working..."));
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: form.attr('action'),
|
||||
data: form.serialize(),
|
||||
success: function(response){
|
||||
$("#account-change-status").html(response.message);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -30,6 +30,10 @@ urlpatterns = ('', # nopep8
|
||||
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
|
||||
|
||||
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"),
|
||||
url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing',
|
||||
name='manage_user_standing'),
|
||||
url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax',
|
||||
name="disable_account_ajax"),
|
||||
|
||||
url(r'^login_ajax$', 'student.views.login_user', name="login"),
|
||||
url(r'^login_ajax/(?P<error>[^/]*)$', 'student.views.login_user'),
|
||||
|
||||
Reference in New Issue
Block a user