Commit ce8f3112 by Diana Huang

Add a new last_activity_at field.

TNL-3068
parent 50e7a9d6
# -*- 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']
"""Django models related to teams functionality.""" """Django models related to teams functionality."""
from uuid import uuid4 from uuid import uuid4
import pytz
from datetime import datetime
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
...@@ -23,13 +25,13 @@ class CourseTeam(models.Model): ...@@ -23,13 +25,13 @@ class CourseTeam(models.Model):
course_id = CourseKeyField(max_length=255, db_index=True) course_id = CourseKeyField(max_length=255, db_index=True)
topic_id = models.CharField(max_length=255, db_index=True, blank=True) topic_id = models.CharField(max_length=255, db_index=True, blank=True)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
# last_activity is computed through a query
description = models.CharField(max_length=300) description = models.CharField(max_length=300)
country = CountryField(blank=True) country = CountryField(blank=True)
language = LanguageField( language = LanguageField(
blank=True, blank=True,
help_text=ugettext_lazy("Optional language the team uses as ISO 639-1 code."), 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') users = models.ManyToManyField(User, db_index=True, related_name='teams', through='CourseTeamMembership')
@classmethod @classmethod
...@@ -62,6 +64,7 @@ class CourseTeam(models.Model): ...@@ -62,6 +64,7 @@ class CourseTeam(models.Model):
description=description, description=description,
country=country if country else '', country=country if country else '',
language=language if language else '', language=language if language else '',
last_activity_at=datetime.utcnow().replace(tzinfo=pytz.utc)
) )
return course_team return course_team
...@@ -88,6 +91,14 @@ class CourseTeamMembership(models.Model): ...@@ -88,6 +91,14 @@ class CourseTeamMembership(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
team = models.ForeignKey(CourseTeam, related_name='membership') team = models.ForeignKey(CourseTeam, related_name='membership')
date_joined = models.DateTimeField(auto_now_add=True) 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 @classmethod
def get_memberships(cls, username=None, course_ids=None, team_id=None): def get_memberships(cls, username=None, course_ids=None, team_id=None):
......
...@@ -58,9 +58,10 @@ class CourseTeamSerializer(serializers.ModelSerializer): ...@@ -58,9 +58,10 @@ class CourseTeamSerializer(serializers.ModelSerializer):
"description", "description",
"country", "country",
"language", "language",
"last_activity_at",
"membership", "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): class CourseTeamCreationSerializer(serializers.ModelSerializer):
...@@ -118,8 +119,8 @@ class MembershipSerializer(serializers.ModelSerializer): ...@@ -118,8 +119,8 @@ class MembershipSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
"""Defines meta information for the ModelSerializer.""" """Defines meta information for the ModelSerializer."""
model = CourseTeamMembership model = CourseTeamMembership
fields = ("user", "team", "date_joined") fields = ("user", "team", "date_joined", "last_activity_at")
read_only_fields = ("date_joined",) read_only_fields = ("date_joined", "last_activity_at")
class PaginatedMembershipSerializer(PaginationSerializer): class PaginatedMembershipSerializer(PaginationSerializer):
......
"""Factories for testing the Teams API.""" """Factories for testing the Teams API."""
import pytz
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
import factory import factory
...@@ -8,6 +10,9 @@ from factory.django import DjangoModelFactory ...@@ -8,6 +10,9 @@ from factory.django import DjangoModelFactory
from ..models import CourseTeam, CourseTeamMembership from ..models import CourseTeam, CourseTeamMembership
LAST_ACTIVITY_AT = datetime(2015, 8, 15, 0, 0, 0, tzinfo=pytz.utc)
class CourseTeamFactory(DjangoModelFactory): class CourseTeamFactory(DjangoModelFactory):
"""Factory for CourseTeams. """Factory for CourseTeams.
...@@ -20,8 +25,10 @@ class CourseTeamFactory(DjangoModelFactory): ...@@ -20,8 +25,10 @@ class CourseTeamFactory(DjangoModelFactory):
discussion_topic_id = factory.LazyAttribute(lambda a: uuid4().hex) discussion_topic_id = factory.LazyAttribute(lambda a: uuid4().hex)
name = "Awesome Team" name = "Awesome Team"
description = "A simple description" description = "A simple description"
last_activity_at = LAST_ACTIVITY_AT
class CourseTeamMembershipFactory(DjangoModelFactory): class CourseTeamMembershipFactory(DjangoModelFactory):
"""Factory for CourseTeamMemberships.""" """Factory for CourseTeamMemberships."""
FACTORY_FOR = CourseTeamMembership FACTORY_FOR = CourseTeamMembership
last_activity_at = LAST_ACTIVITY_AT
...@@ -29,9 +29,23 @@ class TeamMembershipTest(SharedModuleStoreTestCase): ...@@ -29,9 +29,23 @@ class TeamMembershipTest(SharedModuleStoreTestCase):
self.team1 = CourseTeamFactory(course_id=COURSE_KEY1, team_id='team1') self.team1 = CourseTeamFactory(course_id=COURSE_KEY1, team_id='team1')
self.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2') self.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2')
self.team_membership11 = CourseTeamMembershipFactory(user=self.user1, team=self.team1) self.team_membership11 = CourseTeamMembership(user=self.user1, team=self.team1)
self.team_membership12 = CourseTeamMembershipFactory(user=self.user2, team=self.team1) self.team_membership11.save()
self.team_membership21 = CourseTeamMembershipFactory(user=self.user1, team=self.team2) 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( @ddt.data(
(None, None, None, 3), (None, None, None, 3),
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Tests for the teams API at the HTTP request level.""" """Tests for the teams API at the HTTP request level."""
import json import json
import pytz
from datetime import datetime
from dateutil import parser
import ddt import ddt
...@@ -13,7 +16,7 @@ from courseware.tests.factories import StaffFactory ...@@ -13,7 +16,7 @@ from courseware.tests.factories import StaffFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.tests.factories import CourseFactory 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 xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from django_comment_common.models import Role, FORUM_ROLE_COMMUNITY_TA from django_comment_common.models import Role, FORUM_ROLE_COMMUNITY_TA
...@@ -540,6 +543,13 @@ class TestCreateTeamAPI(TeamAPITestCase): ...@@ -540,6 +543,13 @@ class TestCreateTeamAPI(TeamAPITestCase):
team_membership = team['membership'] team_membership = team['membership']
del 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. # Verify that the creating user gets added to the team.
self.assertEqual(len(team_membership), 1) self.assertEqual(len(team_membership), 1)
member = team_membership[0]['user'] member = team_membership[0]['user']
...@@ -587,6 +597,7 @@ class TestDetailTeamAPI(TeamAPITestCase): ...@@ -587,6 +597,7 @@ class TestDetailTeamAPI(TeamAPITestCase):
if status == 200: if status == 200:
self.assertEqual(team['description'], self.test_team_1.description) self.assertEqual(team['description'], self.test_team_1.description)
self.assertEqual(team['discussion_topic_id'], self.test_team_1.discussion_topic_id) 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): def test_does_not_exist(self):
self.get_team_detail('no_such_team', 404) self.get_team_detail('no_such_team', 404)
......
...@@ -221,6 +221,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -221,6 +221,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
* language: Optionally specifies which language the team is * language: Optionally specifies which language the team is
associated with. 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. * membership: A list of the users that are members of the team.
See membership endpoint for more detail. See membership endpoint for more detail.
...@@ -449,6 +452,9 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView): ...@@ -449,6 +452,9 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
* membership: A list of the users that are members of the team. See * membership: A list of the users that are members of the team. See
membership endpoint for more detail. 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 For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is stored exactly as specified. The intention is that plain text is
...@@ -740,6 +746,9 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -740,6 +746,9 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
* date_joined: The date and time the membership was created. * 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 For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is stored exactly as specified. The intention is that plain text is
...@@ -942,6 +951,9 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -942,6 +951,9 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
* date_joined: The date and time the membership was created. * 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 For all text fields, clients rendering the values should take care
to HTML escape them to avoid script injections, as the data is to HTML escape them to avoid script injections, as the data is
stored exactly as specified. The intention is that plain text is stored exactly as specified. The intention is that plain text is
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment