Commit de15f8ec by Diana Huang

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

Add a new last_activity_at field.
parents c49d51f9 ce8f3112
# -*- 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."""
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):
......
......@@ -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):
......
"""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
......@@ -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),
......
# -*- 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)
......
......@@ -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
......
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