15 |
15 |
16 logger = logging.getLogger(__name__) |
16 logger = logging.getLogger(__name__) |
17 auth_user_model = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') |
17 auth_user_model = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') |
18 |
18 |
19 class Workspace(models.Model): |
19 class Workspace(models.Model): |
20 |
20 |
21 workspace_guid = models.CharField(max_length=1024, default=uuid.uuid4, unique=True, blank=False, null=False) # typically UUID |
21 workspace_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False) |
22 title = models.CharField(max_length=1024, null=True) |
22 title = models.CharField(max_length=1024, null=True) |
23 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="workspace_creator") |
23 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="workspace_creator") |
24 creation_date = models.DateTimeField(auto_now_add=True) |
24 creation_date = models.DateTimeField(auto_now_add=True) |
25 |
25 |
26 @property |
26 @property |
27 def renkan_count(self): |
27 def renkan_count(self): |
28 return Renkan.objects.filter(workspace__workspace_guid=self.workspace_guid).count() |
28 #TODO: check count and related objects |
29 |
29 #return Renkan.objects.filter(workspace__workspace_guid=self.workspace_guid).count() |
|
30 return Renkan.objects.filter(workspace_guid=self.workspace_guid).count() |
|
31 |
30 class Meta: |
32 class Meta: |
31 app_label = 'renkanmanager' |
33 app_label = 'renkanmanager' |
32 permissions = ( |
34 permissions = ( |
33 ('view_workspace', 'Can view workspace'), |
35 ('view_workspace', 'Can view workspace'), |
34 ) |
36 ) |
35 |
37 |
36 |
38 |
37 class RenkanManager(models.Manager): |
39 class RenkanManager(models.Manager): |
38 |
40 |
39 @transaction.atomic |
41 @transaction.atomic |
40 def create_renkan(self, creator, title='', content='', source_revision=None, workspace = None): |
42 def create_renkan(self, creator, title='', content='', source_revision=None, workspace = None): |
41 new_renkan = Renkan() |
43 new_renkan = Renkan() |
42 new_renkan.creator = creator |
44 new_renkan.creator = creator |
|
45 #TODO: !!! new_renkan_workspace_guid is not set on the new renkan ! only on the content ! |
43 new_renkan_workspace_guid = "" |
46 new_renkan_workspace_guid = "" |
44 new_renkan_title = title |
47 new_renkan_title = title |
45 new_renkan_content = content |
48 new_renkan_content = content |
46 if workspace is not None: |
49 if workspace is not None: |
47 new_renkan.workspace = workspace |
50 new_renkan.workspace = workspace |
62 initial_revision.title = new_renkan_title if new_renkan_title else "Untitled Renkan" |
65 initial_revision.title = new_renkan_title if new_renkan_title else "Untitled Renkan" |
63 if new_renkan_content: |
66 if new_renkan_content: |
64 new_renkan_content_dict = json.loads(new_renkan.validate_json_content(new_renkan_content)) |
67 new_renkan_content_dict = json.loads(new_renkan.validate_json_content(new_renkan_content)) |
65 new_renkan_content_dict["created"] = str(initial_revision.creation_date) |
68 new_renkan_content_dict["created"] = str(initial_revision.creation_date) |
66 new_renkan_content_dict["updated"] = str(initial_revision.modification_date) |
69 new_renkan_content_dict["updated"] = str(initial_revision.modification_date) |
67 else: |
70 else: |
68 new_renkan_content_dict = { |
71 new_renkan_content_dict = { |
69 "id": str(new_renkan.renkan_guid), |
72 "id": str(new_renkan.renkan_guid), |
70 "title": initial_revision.title, |
73 "title": initial_revision.title, |
71 "description": "", |
74 "description": "", |
72 "created": str(initial_revision.creation_date), |
75 "created": str(initial_revision.creation_date), |
78 "views": [] |
81 "views": [] |
79 } |
82 } |
80 initial_revision.content = json.dumps(new_renkan_content_dict) |
83 initial_revision.content = json.dumps(new_renkan_content_dict) |
81 initial_revision.save() |
84 initial_revision.save() |
82 return new_renkan |
85 return new_renkan |
83 |
86 |
84 class Renkan(models.Model): |
87 class Renkan(models.Model): |
85 |
88 |
86 renkan_guid = models.CharField(max_length=256, default=uuid.uuid4, unique=True, blank=False, null=False) # typically UUID |
89 renkan_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False) |
87 workspace = models.ForeignKey('Workspace', null=True, blank=True, to_field='workspace_guid') |
90 workspace_guid = models.CharField(max_length=256, blank=True, null=True) |
88 source_revision = models.ForeignKey('Revision', null=True, blank=True, related_name="renkan_source_revision", to_field='revision_guid', on_delete=models.SET_NULL) |
91 current_revision_guid = models.CharField(max_length=256, blank=True, null=True) |
|
92 source_revision_guid = models.CharField(max_length=256, blank=True, null=True) |
|
93 |
89 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="renkan_creator") |
94 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="renkan_creator") |
90 creation_date = models.DateTimeField(auto_now_add=True) |
95 creation_date = models.DateTimeField(auto_now_add=True) |
91 state = models.IntegerField(default=1) |
96 state = models.IntegerField(default=1) |
92 |
97 |
93 objects = RenkanManager() |
98 objects = RenkanManager() |
94 |
99 |
95 @property |
100 @property |
96 def revision_count(self): |
101 def revision_count(self): |
97 return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).count() |
102 #TODO: check related object count |
98 |
103 return Revision.objects.filter(parent_renkan_guid=self.renkan_guid).count() |
|
104 #return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).count() |
|
105 |
99 @property |
106 @property |
100 def is_copy(self): |
107 def is_copy(self): |
101 return bool(self.source_revision) |
108 #return bool(self.source_revision) |
102 |
109 return bool(self.source_revision_guid) |
|
110 |
103 # Current revision object or None if there is none |
111 # Current revision object or None if there is none |
104 @property |
112 # @property |
105 def current_revision(self): |
113 # def current_revision(self): |
106 return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).order_by('-creation_date').first() |
114 # return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).order_by('-creation_date').first() |
107 |
115 |
108 # Current revision title |
116 # Current revision title |
109 @property |
117 @property |
110 def title(self): |
118 def title(self): |
111 if self.current_revision: |
119 current_revision = Revision.objects.get(revision_guid = self.current_revision_guid) |
112 return self.current_revision.title |
120 return current_revision.title |
113 else: |
121 #TODO: not good -> 2 requests |
114 return '' |
122 #if self.current_revision: |
115 |
123 # return self.current_revision.title |
|
124 #else: |
|
125 # return '' |
|
126 |
116 # Current revision content |
127 # Current revision content |
117 @property |
128 @property |
118 def content(self): |
129 def content(self): |
119 if self.current_revision: |
130 #TODO: not good -> 2 requests |
120 return self.current_revision.content |
131 current_revision = Revision.objects.get(revision_guid = self.current_revision_guid) |
121 else: |
132 return current_revision.content |
122 return '' |
133 #if self.current_revision: |
123 |
134 # return self.current_revision.content |
|
135 #else: |
|
136 # return '' |
|
137 |
124 def __unicode__(self): |
138 def __unicode__(self): |
125 return self.renkan_guid |
139 return self.renkan_guid |
126 |
140 |
127 def __str__(self): |
141 def __str__(self): |
128 return self.renkan_guid |
142 return self.renkan_guid |
129 |
143 |
130 @transaction.atomic |
144 @transaction.atomic |
131 def save_renkan(self, updator, timestamp="", title="", content="", create_new_revision=False): |
145 def save_renkan(self, updator, timestamp="", title="", content="", create_new_revision=False): |
132 """ |
146 """ |
133 Saves over current revision or saves a new revision entirely. |
147 Saves over current revision or saves a new revision entirely. |
134 Timestamp must be the current revision modification_date. |
148 Timestamp must be the current revision modification_date. |
135 """ |
149 """ |
136 if (not timestamp) or ((self.current_revision is not None) and dateparse.parse_datetime(timestamp) < self.current_revision.modification_date): |
150 if (not timestamp) or ((self.current_revision is not None) and dateparse.parse_datetime(timestamp) < self.current_revision.modification_date): |
137 logger.error("SAVING RENKAN: provided timestamp is %r, which isn't current revision modification_date %r", timestamp, self.current_revision.modification_date) |
151 logger.error("SAVING RENKAN: provided timestamp is %r, which isn't current revision modification_date %r", timestamp, self.current_revision.modification_date) |
138 raise ValidationError(_("Cannot save, provided timestamp is invalid")) |
152 raise ValidationError(_("Cannot save, provided timestamp is invalid")) |
142 if create_new_revision: |
156 if create_new_revision: |
143 revision_to_update = Revision(parent_renkan=self) |
157 revision_to_update = Revision(parent_renkan=self) |
144 revision_to_update.creator = updator |
158 revision_to_update.creator = updator |
145 else: |
159 else: |
146 revision_to_update = Revision.objects.select_for_update().get(revision_guid=self.current_revision.revision_guid) |
160 revision_to_update = Revision.objects.select_for_update().get(revision_guid=self.current_revision.revision_guid) |
147 |
161 |
148 updated_content = self.validate_json_content(content) if content else current_revision.content |
162 updated_content = self.validate_json_content(content) if content else current_revision.content |
149 updated_content_dict = json.loads(updated_content) |
163 updated_content_dict = json.loads(updated_content) |
150 |
164 |
151 # If title is passed as arg to the method, update the title in the json |
165 # If title is passed as arg to the method, update the title in the json |
152 if title: |
166 if title: |
153 updated_title = title |
167 updated_title = title |
154 updated_content_dict["title"] = title |
168 updated_content_dict["title"] = title |
155 # If it is not, we use the one in the json instead |
169 # If it is not, we use the one in the json instead |
156 else: |
170 else: |
157 updated_title = updated_content_dict["title"] |
171 updated_title = updated_content_dict["title"] |
158 |
172 |
159 revision_to_update.modification_date = timezone.now() |
173 revision_to_update.modification_date = timezone.now() |
160 updated_content_dict["updated"] = str(revision_to_update.modification_date) |
174 updated_content_dict["updated"] = str(revision_to_update.modification_date) |
161 updated_content = json.dumps(updated_content_dict) |
175 updated_content = json.dumps(updated_content_dict) |
162 revision_to_update.title = updated_title |
176 revision_to_update.title = updated_title |
163 revision_to_update.content = updated_content |
177 revision_to_update.content = updated_content |
164 if dt_timestamp == revision_to_update.modification_date: |
178 if dt_timestamp == revision_to_update.modification_date: |
165 revision_to_update.modification_date += datetime.resolution |
179 revision_to_update.modification_date += datetime.resolution |
166 revision_to_update.last_updated_by = updator |
180 revision_to_update.last_updated_by = updator |
167 revision_to_update.save() |
181 revision_to_update.save() |
168 |
182 |
169 def validate_json_content(self, content): |
183 def validate_json_content(self, content): |
170 """ |
184 """ |
171 Checks that the json content is valid (keys and structures), raise a ValidationError if format is wrong or value is wrong (for ids), |
185 Checks that the json content is valid (keys and structures), raise a ValidationError if format is wrong or value is wrong (for ids), |
172 if a key is missing, autocompletes with the empty default value |
186 if a key is missing, autocompletes with the empty default value |
173 |
187 |
174 Returns the validated json string |
188 Returns the validated json string |
175 """ |
189 """ |
176 try: |
190 try: |
177 content_to_validate_dict = json.loads(content) |
191 content_to_validate_dict = json.loads(content) |
178 except ValueError: |
192 except ValueError: |
196 content_to_validate_dict["edges"] = [] |
210 content_to_validate_dict["edges"] = [] |
197 if "views" not in content_to_validate_dict: |
211 if "views" not in content_to_validate_dict: |
198 content_to_validate_dict["views"] = [] |
212 content_to_validate_dict["views"] = [] |
199 if "users" not in content_to_validate_dict: |
213 if "users" not in content_to_validate_dict: |
200 content_to_validate_dict["users"] = [] |
214 content_to_validate_dict["users"] = [] |
201 |
215 |
202 if type(content_to_validate_dict["nodes"]) is not list: |
216 if type(content_to_validate_dict["nodes"]) is not list: |
203 raise ValidationError("Provided content has an invalid 'nodes' key: not a list") |
217 raise ValidationError("Provided content has an invalid 'nodes' key: not a list") |
204 if type(content_to_validate_dict["edges"]) is not list: |
218 if type(content_to_validate_dict["edges"]) is not list: |
205 raise ValidationError("Provided content has an invalid 'edges' key: not a list") |
219 raise ValidationError("Provided content has an invalid 'edges' key: not a list") |
206 if type(content_to_validate_dict["views"]) is not list: |
220 if type(content_to_validate_dict["views"]) is not list: |
207 raise ValidationError("Provided content has an invalid 'views' key: not a list") |
221 raise ValidationError("Provided content has an invalid 'views' key: not a list") |
208 if type(content_to_validate_dict["users"]) is not list: |
222 if type(content_to_validate_dict["users"]) is not list: |
209 raise ValidationError("Provided content has an invalid 'users' key: not a list") |
223 raise ValidationError("Provided content has an invalid 'users' key: not a list") |
210 return json.dumps(content_to_validate_dict) |
224 return json.dumps(content_to_validate_dict) |
211 |
225 |
212 @transaction.atomic |
226 #TODO: |
213 def delete(self): |
227 # @transaction.atomic |
214 """ |
228 # def delete(self): |
215 Deleting a renkan also deletes every related revision |
229 # """ |
216 """ |
230 # Deleting a renkan also deletes every related revision |
217 renkan_revisions = Revision.objects.filter(parent_renkan__renkan_guid = self.renkan_guid) |
231 # """ |
218 for child_revision in renkan_revisions: |
232 # renkan_revisions = Revision.objects.filter(parent_renkan__renkan_guid = self.renkan_guid) |
219 child_revision.delete() |
233 # for child_revision in renkan_revisions: |
220 super(Renkan, self).delete() |
234 # child_revision.delete() |
221 |
235 # super(Renkan, self).delete() |
|
236 |
222 class Meta: |
237 class Meta: |
223 app_label = 'renkanmanager' |
238 app_label = 'renkanmanager' |
224 permissions = ( |
239 permissions = ( |
225 ('view_renkan', 'Can view renkan'), |
240 ('view_renkan', 'Can view renkan'), |
226 ) |
241 ) |
227 |
242 |
228 |
243 |
229 class Revision(models.Model): |
244 class Revision(models.Model): |
230 |
245 |
231 revision_guid = models.CharField(max_length=256, default=uuid.uuid4, unique=True) # typically UUID |
246 revision_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False) |
232 parent_renkan = models.ForeignKey('Renkan', null=False, blank=False, to_field='renkan_guid') |
247 parent_renkan_guid = models.CharField(max_length=256) |
|
248 #parent_renkan = models.ForeignKey('Renkan', null=False, blank=False, to_field='renkan_guid') |
233 title = models.CharField(max_length=1024, null=True, blank=True) |
249 title = models.CharField(max_length=1024, null=True, blank=True) |
234 content = models.TextField(blank=True, null=True) |
250 content = models.TextField(blank=True, null=True) |
235 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_creator") |
251 creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_creator") |
236 last_updated_by = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_last_updated_by") |
252 last_updated_by = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_last_updated_by") |
237 creation_date = models.DateTimeField(auto_now_add=True) |
253 creation_date = models.DateTimeField(auto_now_add=True) |
238 modification_date = models.DateTimeField() |
254 modification_date = models.DateTimeField(auto_now=True) |
239 |
255 #modification_date = models.DateTimeField() |
|
256 |
240 @property |
257 @property |
241 def is_current_revision(self): |
258 def is_current_revision(self): |
|
259 try: |
|
260 parent_project = Renkan.objects.get(renkan_guid=self.parent_renkan_guid) |
|
261 except Renkan.DoesNotExist: # SHOULD NOT HAPPEN! |
|
262 raise Http404 |
|
263 return parent_project.current_revision_guid == self.revision_guid |
242 # No need to check if parent_renkan.current_revision is not None, as it won't be if we're calling from a revision |
264 # No need to check if parent_renkan.current_revision is not None, as it won't be if we're calling from a revision |
243 return self.parent_renkan.current_revision.revision_guid == self.revision_guid |
265 #return self.parent_renkan.current_revision.revision_guid == self.revision_guid |
244 |
266 |
245 class Meta: |
267 class Meta: |
246 app_label = 'renkanmanager' |
268 app_label = 'renkanmanager' |
247 permissions = ( |
269 permissions = ( |
248 ('view_revision', 'Can view revision'), |
270 ('view_revision', 'Can view revision'), |
249 ) |
271 ) |
250 |
|