1
- from django .core .urlresolvers import NoReverseMatch
2
- from rest_framework .relations import HyperlinkedRelatedField , RelatedField
1
+ from django .core .urlresolvers import resolve , NoReverseMatch , Resolver404
2
+ from rest_framework .relations import (HyperlinkedRelatedField , RelatedField ,
3
+ get_attribute )
3
4
from rest_framework .reverse import reverse
4
- from rest_framework .serializers import Field
5
+ from rest_framework .serializers import ReadOnlyField , ModelSerializer , SerializerMethodField
5
6
6
- from editorsnotes .main .models import Topic , TopicAssignment
7
+ from editorsnotes .main .models import Topic , TopicAssignment , Project
7
8
8
9
def nested_getattr (obj , attr_string ):
9
10
for attr in attr_string .split ('.' ):
10
11
obj = getattr (obj , attr )
11
12
return obj
12
13
13
- class URLField (Field ):
14
+ class CurrentProjectDefault :
15
+ def set_context (self , serializer_field ):
16
+ self .project = serializer_field .context ['request' ].project
17
+
18
+ def __call__ (self ):
19
+ return self .project
20
+
21
+ def __repr__ (self ):
22
+ return u'%s()' % self .__class__ .__name__
23
+
24
+ class URLField (ReadOnlyField ):
14
25
"""
15
26
An identity URL field.
16
27
@@ -26,113 +37,119 @@ def _get_default_view_name(self, obj):
26
37
return 'api:api-{}-detail' .format (obj .__class__ ._meta .verbose_name_plural [:])
27
38
def _get_lookup_args (self , obj ):
28
39
return tuple (nested_getattr (obj , attr ) for attr in self .lookup_args )
29
- def field_to_native (self , obj , field ):
30
- view = self .view_name or self ._get_default_view_name (obj )
31
- args = self ._get_lookup_args (obj )
40
+ def get_attribute (self , obj ):
41
+ return obj
42
+ def to_representation (self , value ):
43
+ view = self .view_name or self ._get_default_view_name (value )
44
+ args = self ._get_lookup_args (value )
32
45
return reverse (view , args = args , request = self .context ['request' ])
33
46
34
- class ProjectSlugField (Field ):
35
- read_only = True
36
- def field_to_native (self , obj , field_name ):
37
- project = obj .get_affiliation ()
38
- url = reverse ('api:api-project-detail' , args = (project .slug ,),
47
+ class ProjectSlugField (ReadOnlyField ):
48
+ def __init__ (self , * args , ** kwargs ):
49
+ self .queryset = Project .objects .all ()
50
+ super (ProjectSlugField , self ).__init__ (* args , ** kwargs )
51
+ def get_attribute (self , obj ):
52
+ return obj .get_affiliation ()
53
+ def to_representation (self , value ):
54
+ url = reverse ('api:api-project-detail' , args = (value .slug ,),
39
55
request = self .context ['request' ])
40
- return { 'name' : project .name , 'url' : url }
56
+ return { 'name' : value .name , 'url' : url }
41
57
42
58
class HyperlinkedProjectItemField (HyperlinkedRelatedField ):
43
- def to_native (self , obj ):
59
+ def get_attribute (self , obj ):
60
+ return get_attribute (obj , self .source_attrs )
61
+ def to_representation (self , value ):
44
62
"""
45
63
Return URL from item requiring project slug kwarg
46
64
"""
47
65
try :
48
66
return reverse (
49
- self .view_name , args = [obj .project .slug , obj .id ],
67
+ self .view_name , args = [value .project .slug , value .id ],
50
68
request = self .context .get ('request' , None ),
51
69
format = self .format or self .context .get ('format' , None ))
52
70
except NoReverseMatch :
53
71
raise Exception ('Could not resolve URL for document.' )
54
72
55
- class ProjectSpecificItemMixin (object ):
56
- """
57
- Sets a restored instance's `project` attribute based on the serializer's
58
- context.
59
- """
60
- def __init__ (self , * args , ** kwargs ):
61
- super (ProjectSpecificItemMixin , self ).__init__ (* args , ** kwargs )
62
- if self .object is None and 'project' not in self .context :
63
- # FIXME: is this the best error to raise?
64
- raise ValueError (
65
- 'Unbound instances of {0} must be instantiated with a context '
66
- 'object containing a project, e.g.: '
67
- '{0}(context={{\' project\' : project_instance}})' .format (
68
- self .__class__ .__name__ ))
69
- def restore_object (self , attrs , instance = None ):
70
- instance = super (ProjectSpecificItemMixin , self ).restore_object (attrs , instance )
71
- if not instance .pk :
72
- instance .project = self .context ['project' ]
73
- elif 'project' in self .context and instance .project != self .context ['project' ]:
74
- raise ValueError ('Can\' t change project from serializer.' )
75
- return instance
76
-
77
- class UpdatersField (Field ):
73
+ class UpdatersField (ReadOnlyField ):
78
74
read_only = True
79
- def field_to_native (self , obj , field_name ):
80
- return [u .username for u in obj .get_all_updaters ()]
75
+ def get_attribute (self , obj ):
76
+ return obj .get_all_updaters ()
77
+ def to_representation (self , value ):
78
+ return [u .username for u in value ]
79
+
80
+ class MinimalTopicSerializer (ModelSerializer ):
81
+ url = URLField (lookup_arg_attrs = ('project.slug' , 'topic_node_id' ))
82
+ topic_node_id = SerializerMethodField ()
83
+ class Meta :
84
+ model = Topic
85
+ fields = ('id' , 'topic_node_id' , 'preferred_name' , 'url' ,)
86
+ def get_topic_node_id (self , obj ):
87
+ return obj .topic_node_id
81
88
82
89
class TopicAssignmentField (RelatedField ):
90
+ default_error_messages = {
91
+ 'no_match' : 'No topic matches this URL.' ,
92
+ 'outside_project' : 'Related topics must be within the same project.' ,
93
+ 'bad_path' : 'This URL is not a topic API url.'
94
+ }
83
95
def __init__ (self , * args , ** kwargs ):
96
+ kwargs ['queryset' ] = Topic .objects .all ()
84
97
super (TopicAssignmentField , self ).__init__ (* args , ** kwargs )
85
- self .many = True
86
- def format_topic_assignment (self , ta ):
87
- url = reverse ('api:api-topics-detail' ,
88
- args = (ta .topic .project .slug , ta .topic .id ),
89
- request = self .context ['request' ])
90
- return {
91
- 'id' : ta .topic .id ,
92
- 'preferred_name' : ta .topic .preferred_name ,
93
- 'url' : url
94
- }
95
- def field_to_native (self , obj , field_name ):
96
- if obj is None :
97
- ret = []
98
- else :
99
- ret = [self .format_topic_assignment (ta ) for ta in obj .related_topics .all ()]
100
- return ret
101
- def field_from_native (self , data , files , field_name , into ):
98
+ def get_attribute (self , obj ):
99
+ return [ta .topic for ta in obj .related_topics .all ()]
100
+ def to_representation (self , topics ):
101
+ return [ MinimalTopicSerializer (topic , context = self .context ).data
102
+ for topic in topics ]
103
+ def to_internal_value (self , data ):
102
104
if self .read_only :
103
105
return
104
- into [field_name ] = data .get (field_name , [])
106
+ return [self ._topic_from_url (url ) for url in data ]
107
+ def _topic_from_url (self , url ):
108
+ try :
109
+ match = resolve (url )
110
+ except Resolver404 :
111
+ self .fail ('no_match' )
112
+
113
+ if match .view_name != 'api:api-topics-detail' :
114
+ self .fail ('bad_path' )
115
+
116
+ current_project = self .context ['request' ].project
117
+ lookup_project_slug = match .kwargs .pop ('project_slug' )
118
+ if lookup_project_slug != current_project .slug :
119
+ self .fail ('outside_project' )
120
+
121
+ return self .get_queryset ().get (project = current_project , ** match .kwargs )
105
122
106
123
class RelatedTopicSerializerMixin (object ):
107
124
def save_related_topics (self , obj , topics ):
108
125
"""
109
126
Given an array of names, make sure obj is related to those topics.
110
127
"""
111
- to_create = topics [:]
112
- to_delete = []
113
-
114
- for assignment in obj . related_topics . select_related ( ' topic' ). all ():
115
- topic_name = assignment . topic . preferred_name
116
- if topic_name in topics :
117
- to_create . remove ( topic_name )
118
- else :
119
- to_delete . append ( assignment )
120
-
121
- for assignment in to_delete :
122
- assignment . delete ()
123
-
124
- user = self . context [ 'request' ]. user
125
- project = self .context ['request' ].project
126
-
127
- for topic_name in to_create :
128
- topic = Topic . objects . get_or_create_by_name (
129
- topic_name , project , user )
130
- obj . related_topics . create ( topic = topic , creator_id = user . id )
131
-
132
- def save_object ( self , obj , ** kwargs ):
133
- # Need to change to allow partial updates, etc.
134
- topics = []
135
- if getattr ( obj , '_m2m_data' , None ):
136
- topics = obj . _m2m_data . pop ( 'related_topics' )
137
- super ( RelatedTopicSerializerMixin , self ). save_object ( obj , ** kwargs )
138
- self . save_related_topics ( obj , topics )
128
+ rel_topics = obj . related_topics . select_related ( 'topic' ). all ()
129
+
130
+ new_topics = set ( topics )
131
+ existing_topics = { ta . topic for ta in rel_topics }
132
+
133
+ to_create = new_topics . difference ( existing_topics )
134
+ to_delete = existing_topics . difference ( new_topics )
135
+
136
+ # Delete unused topic assignments
137
+ rel_topics . filter ( topic__in = to_delete ). delete ()
138
+
139
+ # Create new topic assignments
140
+ for topic in to_create :
141
+ obj . related_topics . create (
142
+ topic = topic , creator_id = self .context ['request' ].user . id )
143
+
144
+ def create ( self , validated_data ) :
145
+ topics = validated_data . pop ( 'related_topics' , None )
146
+ obj = super ( RelatedTopicSerializerMixin , self ). create ( validated_data )
147
+ if topics is not None :
148
+ self . save_related_topics ( obj , topics )
149
+ return obj
150
+ def update ( self , instance , validated_data ):
151
+ topics = validated_data . pop ( 'related_topics' , None )
152
+ super ( RelatedTopicSerializerMixin , self ). update ( instance , validated_data )
153
+ if topics is not None :
154
+ self . save_related_topics ( instance , topics )
155
+ return instance
0 commit comments