diff --git a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee index 0fee756658..0b052bc7ba 100644 --- a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee +++ b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee @@ -67,5 +67,6 @@ class @DiscussionSpecHelper data-course-name="Fake Course" data-user-create-comment="true" data-user-create-subcomment="true" + data-read-only="false" > """) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index 5a1a0a5b29..75c6e9d5ac 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -13,7 +13,8 @@ if Backbone? mode: @mode, flagged: @model.isFlagged(), author_display: @getAuthorDisplay(), - cid: @model.cid + cid: @model.cid, + readOnly: $('.discussion-module').data('read-only') }, @model.attributes, ) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_view.coffee index 798a4022b6..f9a6d21b00 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_view.coffee @@ -23,6 +23,8 @@ if Backbone? if @mode not in ["tab", "inline"] throw new Error("invalid mode: " + @mode) + @readOnly = $(".discussion-module").data('read-only') + # Quick fix to have an actual model when we're receiving new models from # the server. @model.collection.on "reset", (collection) => @@ -51,12 +53,15 @@ if Backbone? renderTemplate: -> @template = _.template($("#thread-template").html()) - templateData = @model.toJSON() container = $("#discussion-container") if !container.length # inline discussion container = $(".discussion-module") - templateData.can_create_comment = container.data("user-create-comment") + templateData = _.extend( + @model.toJSON(), + readOnly: @readOnly, + can_create_comment: container.data("user-create-comment") + ) @template(templateData) render: -> diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee index 51432770a2..059e63f65c 100644 --- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee +++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee @@ -9,7 +9,8 @@ if Backbone? _.extend( { cid: @model.cid, - author_display: @getAuthorDisplay() + author_display: @getAuthorDisplay(), + readOnly: $('.discussion-module').data('read-only') }, @model.attributes ) diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee index 6d634dd481..3e72cd56d9 100644 --- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee @@ -10,7 +10,8 @@ if Backbone? { cid: @model.cid, author_display: @getAuthorDisplay(), - endorser_display: @getEndorserDisplay() + endorser_display: @getEndorserDisplay(), + readOnly: $('.discussion-module').data('read-only') }, @model.attributes ) diff --git a/common/static/coffee/src/discussion/views/thread_response_view.coffee b/common/static/coffee/src/discussion/views/thread_response_view.coffee index a299738d6f..9d91d1361c 100644 --- a/common/static/coffee/src/discussion/views/thread_response_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_view.coffee @@ -13,17 +13,21 @@ if Backbone? initialize: (options) -> @collapseComments = options.collapseComments @createShowView() + @readOnly = $('.discussion-module').data('read-only') renderTemplate: -> @template = _.template($("#thread-response-template").html()) - templateData = @model.toJSON() - templateData.wmdId = @model.id ? (new Date()).getTime() container = $("#discussion-container") if !container.length # inline discussion container = $(".discussion-module") - templateData.create_sub_comment = container.data("user-create-subcomment") + templateData = _.extend( + @model.toJSON(), + wmdId: @model.id ? (new Date()).getTime(), + create_sub_comment: container.data("user-create-subcomment"), + readOnly: @readOnly + ) @template(templateData) render: -> @@ -88,7 +92,10 @@ if Backbone? comment.set('thread', @model.get('thread')) view = new ResponseCommentView(model: comment) view.render() - @$el.find(".comments .new-comment").before(view.el) + if @readOnly + @$el.find('.comments').append(view.el) + else + @$el.find(".comments .new-comment").before(view.el) view.bind "comment:edit", (event) => @cancelEdit(event) if @editView? @cancelCommentEdits() diff --git a/common/static/common/templates/discussion/forum-actions.underscore b/common/static/common/templates/discussion/forum-actions.underscore index 9fd9714a3e..5401e41180 100644 --- a/common/static/common/templates/discussion/forum-actions.underscore +++ b/common/static/common/templates/discussion/forum-actions.underscore @@ -1,16 +1,18 @@ - + + +<% } %> diff --git a/common/static/common/templates/discussion/response-comment-show.underscore b/common/static/common/templates/discussion/response-comment-show.underscore index 5f15338f1e..5dbf579afb 100644 --- a/common/static/common/templates/discussion/response-comment-show.underscore +++ b/common/static/common/templates/discussion/response-comment-show.underscore @@ -7,7 +7,8 @@ contentId: cid, contentType: 'comment', primaryActions: [], - secondaryActions: ['edit', 'delete', 'report'] + secondaryActions: ['edit', 'delete', 'report'], + readOnly: readOnly } ) %> diff --git a/common/static/common/templates/discussion/thread-response-show.underscore b/common/static/common/templates/discussion/thread-response-show.underscore index d43fd2d8aa..60b6b82a4d 100644 --- a/common/static/common/templates/discussion/thread-response-show.underscore +++ b/common/static/common/templates/discussion/thread-response-show.underscore @@ -49,7 +49,8 @@ contentId: cid, contentType: 'response', primaryActions: ['vote', thread.get('thread_type') == 'question' ? 'answer' : 'endorse'], - secondaryActions: ['edit', 'delete', 'report'] + secondaryActions: ['edit', 'delete', 'report'], + readOnly: readOnly } ) %> diff --git a/common/static/common/templates/discussion/thread-response.underscore b/common/static/common/templates/discussion/thread-response.underscore index 7e3aeffde2..8032f473da 100644 --- a/common/static/common/templates/discussion/thread-response.underscore +++ b/common/static/common/templates/discussion/thread-response.underscore @@ -12,16 +12,16 @@
  1. - <% if (create_sub_comment) { %> -
    -
      - -
      -
      - <%- gettext("Submit") %> -
      -
      - <% } %> + <% if (create_sub_comment && !readOnly) { %> +
      +
        + +
        +
        + <%- gettext("Submit") %> +
        +
        + <% } %>
      diff --git a/common/static/common/templates/discussion/thread-show.underscore b/common/static/common/templates/discussion/thread-show.underscore index 0b46318bdd..c28e0859c4 100644 --- a/common/static/common/templates/discussion/thread-show.underscore +++ b/common/static/common/templates/discussion/thread-show.underscore @@ -40,19 +40,22 @@ <%- gettext("Closed") %> -
      - <%= - _.template( - $('#forum-actions').html(), - { - contentId: cid, - contentType: 'post', - primaryActions: ['vote', 'follow'], - secondaryActions: ['pin', 'edit', 'delete', 'report', 'close'] - } - ) - %> -
      + <% if (!readOnly) { %> +
      + <%= + _.template( + $('#forum-actions').html(), + { + contentId: cid, + contentType: 'post', + primaryActions: ['vote', 'follow'], + secondaryActions: ['pin', 'edit', 'delete', 'report', 'close'], + readOnly: readOnly + } + ) + %> +
      + <% } %>
      <%- body %>
      diff --git a/common/static/common/templates/discussion/thread.underscore b/common/static/common/templates/discussion/thread.underscore index 3212105475..8498c9ae8d 100644 --- a/common/static/common/templates/discussion/thread.underscore +++ b/common/static/common/templates/discussion/thread.underscore @@ -8,18 +8,20 @@
      -
      - -
      + <% if (!readOnly) { %> +
      + +
      + <% } %>
        - <% if (can_create_comment) { %> + <% if (can_create_comment && !readOnly) { %>

        <%- gettext("Post a response:") %>

          diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 1dff334ed5..a0430b157e 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -3,6 +3,7 @@ Acceptance tests for the teams feature. """ import json +import ddt from nose.plugins.attrib import attr from uuid import uuid4 @@ -652,6 +653,7 @@ class CreateTeamTest(TeamsTabBase): @attr('shard_5') +@ddt.ddt class TeamPageTest(TeamsTabBase): """Tests for viewing a specific team""" def setUp(self): @@ -659,12 +661,11 @@ class TeamPageTest(TeamsTabBase): self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) self.team = self.create_teams(self.topic, 1)[0] - self.create_membership(self.user_info['username'], self.team['id']) self.team_page = TeamPage(self.browser, self.course_id, self.team) def setup_thread(self): """ - Set up the discussion thread for the team. + Create and return a thread for this test's discussion topic. """ thread = Thread( id="test_thread_{}".format(uuid4().hex), @@ -675,15 +676,25 @@ class TeamPageTest(TeamsTabBase): thread_fixture.push() return thread - def test_discussion_on_team_page(self): + def setup_discussion_user(self, role=None, staff=False): + """Set this test's user to have the given role in its + discussions. Role is one of 'Community TA', 'Moderator', + 'Administrator', or 'Student'. """ - Scenario: Team Page renders a team discussion. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic - When a thread exists in the team's discussion - And I visit the Team page for that team - Then I should see a discussion with the correct discussion_id - And I should see the existing thread + kwargs = { + 'course_id': self.course_id, + 'staff': staff + } + if role is not None: + kwargs['roles'] = role + #pylint: disable=attribute-defined-outside-init + self.user_info = AutoAuthPage(self.browser, **kwargs).visit().user_info + + def verify_teams_discussion_permissions(self, should_have_permission): + """Verify that the teams discussion component is in the correct state + for the test user. If `should_have_permission` is True, assert that + the user can see controls for posting replies, voting, editing, and + deleting. Otherwise, assert that those controls are hidden. """ thread = self.setup_thread() self.team_page.visit() @@ -693,3 +704,42 @@ class TeamPageTest(TeamsTabBase): self.assertTrue(discussion.is_discussion_expanded()) self.assertEqual(discussion.get_num_displayed_threads(), 1) self.assertTrue(discussion.has_thread(thread['id'])) + assertion = self.assertTrue if should_have_permission else self.assertFalse + assertion(discussion.q(css='.post-header-actions').present) + assertion(discussion.q(css='.add-response').present) + assertion(discussion.q(css='.new-post-btn').present) + + def test_discussion_on_my_team_page(self): + """ + Scenario: Team Page renders a discussion for a team to which I belong. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am a member + When the team has a discussion with a thread + And I visit the Team page for that team + Then I should see a discussion with the correct discussion_id + And I should see the existing thread + And I should see controls to change the state of the discussion + """ + self.create_membership(self.user_info['username'], self.team['id']) + self.verify_teams_discussion_permissions(True) + + @ddt.data(True, False) + def test_discussion_on_other_team_page(self, is_staff): + """ + Scenario: Team Page renders a team discussion for a team to which I do + not belong. + Given I am enrolled in a course with a team configuration, a topic, + and a team belonging to that topic of which I am not a member + When the team has a discussion with a thread + And I visit the Team page for that team + Then I should see a discussion with the correct discussion_id + And I should see the team's thread + And I should not see controls to change the state of the discussion + """ + self.setup_discussion_user(staff=is_staff) + self.verify_teams_discussion_permissions(False) + + @ddt.data('Moderator', 'Community TA', 'Administrator') + def test_discussion_privileged(self, role): + self.setup_discussion_user(role=role) + self.verify_teams_discussion_permissions(True) diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index eff0e186d4..241ae24cf4 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -22,6 +22,7 @@ from openedx.core.djangoapps.course_groups.tests.helpers import config_course_co from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.util.testing import ContentGroupTestCase +from student.roles import CourseStaffRole from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MIXED_TOY_MODULESTORE from xmodule.modulestore.django import modulestore @@ -80,6 +81,8 @@ class AccessUtilsTestCase(ModuleStoreTestCase): self.community_ta_role.users.add(self.community_ta1) self.community_ta2 = UserFactory(username='community_ta2', email='community_ta2@edx.org') self.community_ta_role.users.add(self.community_ta2) + self.course_staff = UserFactory(username='course_staff', email='course_staff@edx.org') + CourseStaffRole(self.course_id).add_users(self.course_staff) def test_get_role_ids(self): ret = utils.get_role_ids(self.course_id) @@ -89,6 +92,7 @@ class AccessUtilsTestCase(ModuleStoreTestCase): def test_has_discussion_privileges(self): self.assertFalse(utils.has_discussion_privileges(self.student1, self.course_id)) self.assertFalse(utils.has_discussion_privileges(self.student2, self.course_id)) + self.assertFalse(utils.has_discussion_privileges(self.course_staff, self.course_id)) self.assertTrue(utils.has_discussion_privileges(self.moderator, self.course_id)) self.assertTrue(utils.has_discussion_privileges(self.community_ta1, self.course_id)) self.assertTrue(utils.has_discussion_privileges(self.community_ta2, self.course_id)) diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 7b0169f706..8aee23a2d0 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -88,5 +88,35 @@ define([ expectFocus(teamsTabView.$('.warning')); }); }); + + describe('Discussion privileges', function () { + it('allows privileged access to any team', function () { + teamsTabView.$el.data('privileged', true); + // Note: using `undefined` here to ensure that we + // don't even look at the team when the user is + // privileged + expect(teamsTabView.readOnlyDiscussion(undefined)).toBe(false); + }); + + it('allows access to a team which an unprivileged user is a member of', function () { + teamsTabView.$el.data('privileged', false).data('username', 'test-user'); + expect(teamsTabView.readOnlyDiscussion({ + attributes: { + membership: [{ + user: { + username: 'test-user' + } + }] + } + })).toBe(false); + }); + + it('does not allow access if the user is neither privileged nor a team member', function () { + teamsTabView.$el.data('privileged', false).data('username', 'test-user'); + expect(teamsTabView.readOnlyDiscussion({ + attributes: { membership: [] } + })).toBe(true); + }); + }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index a0a0d7794a..dddcafc916 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -10,15 +10,14 @@ initialize: function (options) { this.courseID = options.courseID; this.discussionTopicID = this.model.get('discussion_topic_id'); + this.readOnly = options.readOnly; }, render: function () { - var canPostToTeam = true; // TODO: determine this permission correctly! this.$el.html(_.template(teamTemplate, { courseID: this.courseID, discussionTopicID: this.discussionTopicID, - canCreateComment: canPostToTeam, - canCreateSubComment: canPostToTeam + readOnly: this.readOnly })); this.discussionView = new TeamDiscussionView({ el: this.$('.discussion-module') diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 89de3e7c28..131e8ad50e 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -209,10 +209,12 @@ courseID = this.courseID; self.getTopic(topicID).done(function(topic) { self.getTeam(teamID).done(function(team) { - var view = new TeamProfileView({ - courseID: courseID, - model: team - }); + var readOnly = self.readOnlyDiscussion(team), + view = new TeamProfileView({ + courseID: courseID, + model: team, + readOnly: readOnly + }); deferred.resolve(self.createViewWithHeader(view, team, topic)); }); }); @@ -377,6 +379,23 @@ hideWarning: function () { this.$('.warning').toggleClass('is-hidden', true); + }, + + /** + * Returns true if the discussion thread belonging to + * `team` is accessible to the user. This is the case + * if the user is privileged (i.e., a community TA, + * moderator, or administrator), or if the user + * belongs to the team. + */ + readOnlyDiscussion: function (team) { + var self = this; + return !( + this.$el.data('privileged') || + _.any(team.attributes.membership, function (membership) { + return membership.user.username === self.$el.data('username'); + }) + ); } }); diff --git a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore index bb7ae65320..f524b7939d 100644 --- a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore +++ b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore @@ -1,7 +1,10 @@
          - <%= gettext("New Post") %> + data-read-only="<%= readOnly %>" + data-user-create-comment="<%= !readOnly %>" + data-user-create-subcomment="<%= !readOnly %>"> + <% if ( !readOnly) { %> + <%= gettext("New Post") %> + <% } %>
          diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index a267b96173..d2ac44343a 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -18,7 +18,7 @@
          -
          +
          diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py index 5c12057ff8..b8c637a8f7 100644 --- a/lms/djangoapps/teams/tests/test_views.py +++ b/lms/djangoapps/teams/tests/test_views.py @@ -467,7 +467,7 @@ class TestCreateTeamAPI(TeamAPITestCase): # Verify that the creating user gets added to the team. self.assertEqual(len(team_membership), 1) member = team_membership[0]['user'] - self.assertEqual(member['id'], creator) + self.assertEqual(member['username'], creator) self.assertEqual(team, { 'name': 'Fully specified team', @@ -688,7 +688,7 @@ class TestListMembershipAPI(TeamAPITestCase): membership = self.get_membership_list(status, {'team_id': self.test_team_1.team_id}, user=user) if status == 200: self.assertEqual(membership['count'], 1) - self.assertEqual(membership['results'][0]['user']['id'], self.users['student_enrolled'].username) + self.assertEqual(membership['results'][0]['user']['username'], self.users['student_enrolled'].username) @ddt.data( (None, 401, False), @@ -705,7 +705,7 @@ class TestListMembershipAPI(TeamAPITestCase): if status == 200: if has_content: self.assertEqual(membership['count'], 1) - self.assertEqual(membership['results'][0]['team']['id'], self.test_team_1.team_id) + self.assertEqual(membership['results'][0]['team']['team_id'], self.test_team_1.team_id) else: self.assertEqual(membership['count'], 0) @@ -754,8 +754,8 @@ class TestCreateMembershipAPI(TeamAPITestCase): user=user ) if status == 200: - self.assertEqual(membership['user']['id'], self.users['student_enrolled_not_on_team'].username) - self.assertEqual(membership['team']['id'], self.test_team_1.team_id) + self.assertEqual(membership['user']['username'], self.users['student_enrolled_not_on_team'].username) + self.assertEqual(membership['team']['team_id'], self.test_team_1.team_id) memberships = self.get_membership_list(200, {'team_id': self.test_team_1.team_id}) self.assertEqual(memberships['count'], 2) diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 313b433c9f..cdcd92c08b 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -90,6 +90,7 @@ class TeamsDashboardView(View): instance=topics_page, context={'course_id': course.id, 'sort_order': sort_order} ) + user = request.user context = { "course": course, "topics": topics_serializer.data, @@ -100,6 +101,8 @@ class TeamsDashboardView(View): "teams_url": reverse('teams_list', request=request), "languages": settings.ALL_LANGUAGES, "countries": list(countries), + "username": user.username, + "privileged": has_discussion_privileges(user, course_key) } return render_to_response("teams/teams.html", context) diff --git a/lms/templates/discussion/_discussion_module.html b/lms/templates/discussion/_discussion_module.html index aedf007343..ba6d1c88e9 100644 --- a/lms/templates/discussion/_discussion_module.html +++ b/lms/templates/discussion/_discussion_module.html @@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _ %> -
          +
          ${_("Show Discussion")} % if can_create_thread: ${_("New Post")} diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index 8b074b8190..00fdfd773d 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -31,6 +31,7 @@ from django.core.urlresolvers import reverse data-user-info="${user_info}" data-user-create-comment="${can_create_comment}" data-user-create-subcomment="${can_create_subcomment}" + data-read-only="false" data-threads="${threads}" data-thread-pages="${thread_pages}" data-content-info="${annotated_content_info}" diff --git a/openedx/core/lib/api/serializers.py b/openedx/core/lib/api/serializers.py index 29c20a570b..03ec001137 100644 --- a/openedx/core/lib/api/serializers.py +++ b/openedx/core/lib/api/serializers.py @@ -31,7 +31,6 @@ class PaginationSerializer(pagination.PaginationSerializer): class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): """Serializes arbitrary models in a collapsed format, with just an id and url.""" - id = serializers.CharField(read_only=True) # pylint: disable=invalid-name url = serializers.HyperlinkedIdentityField(view_name='') def __init__(self, model_class, view_name, id_source='id', lookup_field=None, *args, **kwargs): @@ -42,7 +41,8 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): view_name (string): Name of the Django view used to lookup the model. id_source (string): Optional name of the id field on the model. - Defaults to 'id'. + Defaults to 'id'. Also used as the property name of the field + in the serialized representation. lookup_field (string): Optional name of the model field used to lookup the model in the view. Defaults to the value of id_source. @@ -54,7 +54,7 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): super(CollapsedReferenceSerializer, self).__init__(*args, **kwargs) - self.fields['id'].source = id_source + self.fields[id_source] = serializers.CharField(read_only=True, source=id_source) self.fields['url'].view_name = view_name self.fields['url'].lookup_field = lookup_field @@ -63,4 +63,4 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): model is set dynamically in __init__. """ - fields = ("id", "url") + fields = ("url",)