Merge pull request #9360 from edx/diana/add-last-activity

Add a new last_activity_at field.
This commit is contained in:
Diana Huang
2015-08-18 12:03:48 -04:00
7 changed files with 158 additions and 8 deletions

View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
import pytz
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'CourseTeam.last_activity_at'
db.add_column('teams_courseteam', 'last_activity_at',
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2015, 8, 17, 0, 0).replace(tzinfo=pytz.utc)),
keep_default=False)
# Adding field 'CourseTeamMembership.last_activity_at'
db.add_column('teams_courseteammembership', 'last_activity_at',
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2015, 8, 17, 0, 0).replace(tzinfo=pytz.utc)),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CourseTeam.last_activity_at'
db.delete_column('teams_courseteam', 'last_activity_at')
# Deleting field 'CourseTeamMembership.last_activity_at'
db.delete_column('teams_courseteammembership', 'last_activity_at')
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'})
},
'teams.courseteam': {
'Meta': {'object_name': 'CourseTeam'},
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
'discussion_topic_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'language': ('student.models.LanguageField', [], {'max_length': '16', 'blank': 'True'}),
'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'team_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'topic_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'teams'", 'symmetrical': 'False', 'through': "orm['teams.CourseTeamMembership']", 'to': "orm['auth.User']"})
},
'teams.courseteammembership': {
'Meta': {'unique_together': "(('user', 'team'),)", 'object_name': 'CourseTeamMembership'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}),
'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'membership'", 'to': "orm['teams.CourseTeam']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['teams']

View File

@@ -1,6 +1,8 @@
"""Django models related to teams functionality."""
from uuid import uuid4
import pytz
from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
@@ -23,13 +25,13 @@ class CourseTeam(models.Model):
course_id = CourseKeyField(max_length=255, db_index=True)
topic_id = models.CharField(max_length=255, db_index=True, blank=True)
date_created = models.DateTimeField(auto_now_add=True)
# last_activity is computed through a query
description = models.CharField(max_length=300)
country = CountryField(blank=True)
language = LanguageField(
blank=True,
help_text=ugettext_lazy("Optional language the team uses as ISO 639-1 code."),
)
last_activity_at = models.DateTimeField()
users = models.ManyToManyField(User, db_index=True, related_name='teams', through='CourseTeamMembership')
@classmethod
@@ -62,6 +64,7 @@ class CourseTeam(models.Model):
description=description,
country=country if country else '',
language=language if language else '',
last_activity_at=datetime.utcnow().replace(tzinfo=pytz.utc)
)
return course_team
@@ -88,6 +91,14 @@ class CourseTeamMembership(models.Model):
user = models.ForeignKey(User)
team = models.ForeignKey(CourseTeam, related_name='membership')
date_joined = models.DateTimeField(auto_now_add=True)
last_activity_at = models.DateTimeField()
def save(self, *args, **kwargs):
""" Customize save method to set the last_activity_at if it does not currently exist. """
if not self.last_activity_at:
self.last_activity_at = datetime.utcnow().replace(tzinfo=pytz.utc)
super(CourseTeamMembership, self).save(*args, **kwargs)
@classmethod
def get_memberships(cls, username=None, course_ids=None, team_id=None):

View File

@@ -58,9 +58,10 @@ class CourseTeamSerializer(serializers.ModelSerializer):
"description",
"country",
"language",
"last_activity_at",
"membership",
)
read_only_fields = ("course_id", "date_created", "discussion_topic_id")
read_only_fields = ("course_id", "date_created", "discussion_topic_id", "last_activity_at")
class CourseTeamCreationSerializer(serializers.ModelSerializer):
@@ -118,8 +119,8 @@ class MembershipSerializer(serializers.ModelSerializer):
class Meta(object):
"""Defines meta information for the ModelSerializer."""
model = CourseTeamMembership
fields = ("user", "team", "date_joined")
read_only_fields = ("date_joined",)
fields = ("user", "team", "date_joined", "last_activity_at")
read_only_fields = ("date_joined", "last_activity_at")
class PaginatedMembershipSerializer(PaginationSerializer):

View File

@@ -1,5 +1,7 @@
"""Factories for testing the Teams API."""
import pytz
from datetime import datetime
from uuid import uuid4
import factory
@@ -8,6 +10,9 @@ from factory.django import DjangoModelFactory
from ..models import CourseTeam, CourseTeamMembership
LAST_ACTIVITY_AT = datetime(2015, 8, 15, 0, 0, 0, tzinfo=pytz.utc)
class CourseTeamFactory(DjangoModelFactory):
"""Factory for CourseTeams.
@@ -20,8 +25,10 @@ class CourseTeamFactory(DjangoModelFactory):
discussion_topic_id = factory.LazyAttribute(lambda a: uuid4().hex)
name = "Awesome Team"
description = "A simple description"
last_activity_at = LAST_ACTIVITY_AT
class CourseTeamMembershipFactory(DjangoModelFactory):
"""Factory for CourseTeamMemberships."""
FACTORY_FOR = CourseTeamMembership
last_activity_at = LAST_ACTIVITY_AT

View File

@@ -29,9 +29,23 @@ class TeamMembershipTest(SharedModuleStoreTestCase):
self.team1 = CourseTeamFactory(course_id=COURSE_KEY1, team_id='team1')
self.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2')
self.team_membership11 = CourseTeamMembershipFactory(user=self.user1, team=self.team1)
self.team_membership12 = CourseTeamMembershipFactory(user=self.user2, team=self.team1)
self.team_membership21 = CourseTeamMembershipFactory(user=self.user1, team=self.team2)
self.team_membership11 = CourseTeamMembership(user=self.user1, team=self.team1)
self.team_membership11.save()
self.team_membership12 = CourseTeamMembership(user=self.user2, team=self.team1)
self.team_membership12.save()
self.team_membership21 = CourseTeamMembership(user=self.user1, team=self.team2)
self.team_membership21.save()
def test_membership_last_activity_set(self):
current_last_activity = self.team_membership11.last_activity_at
# Assert that the first save in the setUp sets a value.
self.assertIsNotNone(current_last_activity)
self.team_membership11.save()
# Verify that we only change the last activity_at when it doesn't
# already exist.
self.assertEqual(self.team_membership11.last_activity_at, current_last_activity)
@ddt.data(
(None, None, None, 3),

View File

@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""Tests for the teams API at the HTTP request level."""
import json
import pytz
from datetime import datetime
from dateutil import parser
import ddt
@@ -13,7 +16,7 @@ from courseware.tests.factories import StaffFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment
from xmodule.modulestore.tests.factories import CourseFactory
from .factories import CourseTeamFactory
from .factories import CourseTeamFactory, LAST_ACTIVITY_AT
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from django_comment_common.models import Role, FORUM_ROLE_COMMUNITY_TA
@@ -542,6 +545,13 @@ class TestCreateTeamAPI(TeamAPITestCase):
team_membership = team['membership']
del team['membership']
# verify that it's been set to a time today.
self.assertEqual(
parser.parse(team['last_activity_at']).date(),
datetime.utcnow().replace(tzinfo=pytz.utc).date()
)
del team['last_activity_at']
# Verify that the creating user gets added to the team.
self.assertEqual(len(team_membership), 1)
member = team_membership[0]['user']
@@ -590,6 +600,7 @@ class TestDetailTeamAPI(TeamAPITestCase):
if status == 200:
self.assertEqual(team['description'], self.test_team_1.description)
self.assertEqual(team['discussion_topic_id'], self.test_team_1.discussion_topic_id)
self.assertEqual(parser.parse(team['last_activity_at']), LAST_ACTIVITY_AT)
def test_does_not_exist(self):
self.get_team_detail('no_such_team', 404)

View File

@@ -222,6 +222,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
* language: Optionally specifies which language the team is
associated with.
* last_activity_at: The date of the last activity of any team member
within the team.
* membership: A list of the users that are members of the team.
See membership endpoint for more detail.
@@ -451,6 +454,9 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
* membership: A list of the users that are members of the team. See
membership endpoint for more detail.
* last_activity_at: The date of the last activity of any team member
within the team.
For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is
@@ -754,6 +760,9 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
* date_joined: The date and time the membership was created.
* last_activity_at: The date of the last activity of the user
within the team.
For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is
@@ -958,6 +967,9 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
* date_joined: The date and time the membership was created.
* last_activity_at: The date of the last activity of any team member
within the team.
For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is