Merge pull request #9360 from edx/diana/add-last-activity
Add a new last_activity_at field.
This commit is contained in:
@@ -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']
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user