-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocuments_controller.py
More file actions
256 lines (209 loc) · 9.15 KB
/
documents_controller.py
File metadata and controls
256 lines (209 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env python
import os
import sys
import logging
import sqlite3
import optparse
import simplejson
import diff_match_patch
import documents_service
UNKNOWN_ID = ""
UNKNOWN_VERSION = -1
"""
Sync model for syncing documents hosted by the open source "Documents.com" service to local state:
http://github.com/jessegrosjean/Documents.com.client.python
Note that WriteRoom and TaskPaper for iPhone's document sharing feature works by starting a simplifiled
"Documents.com" web server. That means that you can also use this model to sync documents directly from WriteRoom
or TaskPaper for iPhone when document sharing is turned on.
Example:
import logging
import documents_service
import documents_controller
logging.basicConfig(level=logging.INFO)
controller = documents_controller.DocumentController(documents_service.DocumentsService("www.simpletext.ws", "google_id", "google_pass", "simpletextws"))
controller.sync_documents()
# At this point server state is synced down. Try calling sync_documents() again and note that the response is much
# faster (assuming you had documents on the server the first time around) because there are no changes to sync.
controller.sync_documents()
# Try making a change on the server and then call sync_documents() again, that change is synced down.
controller.sync_documents()
# Change some local state and sync again, then look at web server, document on server should also be updated.
controller.documents[0].content = "new content"
controller.sync_documents()
"""
class Document(object):
def __init__(self, data):
self.server_version = UNKNOWN_VERSION
self.name = data.get('name')
self.tags = data.get('tags')
self.user_ids = data.get('user_ids')
self.content = data.get('content')
self.shadow_id = data.get('id', UNKNOWN_ID)
self.shadow_version = data.get('version')
self.shadow_tags = data.get('tags')
self.shadow_name = data.get('name')
self.shadow_user_ids = data.get('user_ids')
self.shadow_content = data.get('content')
self.is_deleted_from_server = False
self.is_deleted_from_client = False
def __str__( self ):
return "%s %s %s" % (self.name, self.shadow_id, self.shadow_version)
def local_edits(self):
if self.shadow_id != None and self.shadow_version != None:
edits = { 'version' : self.shadow_version }
if self.name != self.shadow_name:
edits['name'] = name
if self.content != self.shadow_content:
dmp = diff_match_patch.diff_match_patch()
edits['patches'] = dmp.patch_toText(dmp.patch_make(self.shadow_content, self.content))
if self.tags != self.shadow_tags:
edits['tags_added'] = filter(lambda tag: tag not in self.shadow_tags, self.tags)
edits['tags_removed'] = filter(lambda tag: tag not in self.tags, self.shadow_tags)
if self.user_ids != self.shadow_user_ids:
edits['user_ids_added'] = filter(lambda tag: tag not in self.shadow_user_ids, self.user_ids)
edits['user_ids_removed'] = filter(lambda tag: tag not in self.user_ids, self.shadow_user_ids)
if len(edits) > 1:
return edits
return None
def has_local_edits(self):
return self.local_edits() != None
def has_server_edits(self):
return self.shadow_version != self.server_version
def is_server_document(self):
return self.shadow_id != UNKNOWN_ID
def is_inserted_from_server(self):
return self.server_version != UNKNOWN_VERSION and self.shadow_version == None
def GET(self, controller):
logging.info("GETTING: %s", self)
return self.handle_sync_response(controller.rest_service.GET_document(self.shadow_id), controller)
def PUT(self, controller):
logging.info("PUTTING: %s", self)
edits = self.local_edits()
return self.handle_sync_response(controller.rest_service.PUT_document(
self.shadow_id,
self.shadow_version,
name=edits.get('name'),
tags_added=edits.get('tags_added'),
tags_removed=edits.get('tags_removed'),
user_ids_added=edits.get('user_ids_added'),
user_ids_removed=edits.get('user_ids_removed'),
patches=edits.get('patches')), controller)
def POST(self, controller):
logging.info("POSTING: %s", self)
return self.handle_sync_response(controller.rest_service.POST_document(self.name, tags=self.tags, user_ids=self.user_ids, content=self.content), controller)
def DELETE(self, controller):
logging.info("DELETEING: %s", self)
return self.handle_sync_response(controller.rest_service.DELETE_document(self.shadow_id, self.shadow_version), controller)
def sync(self, controller):
# Handle delete cases.
if self.is_deleted_from_server:
if self.has_local_edits():
self.is_deleted_from_server = False
return self.POST(controller)
else:
controller.delete_document(self)
return None
elif self.is_deleted_from_client:
if self.has_server_edits():
self.is_deleted_from_client = False
return self.GET(controller)
else:
return self.DELETE(controller)
# Handle create cases.
if not self.is_server_document():
return self.POST(controller)
elif self.is_inserted_from_server():
return self.GET(controller)
# Handle changes.
if self.has_local_edits():
return self.PUT(controller)
elif self.has_server_edits():
return self.GET(controller)
return None
def handle_sync_response(self, response, controller):
if self.is_deleted_from_client:
controller.delete_document(self)
return None
if response.get('id'): self.shadow_id = response['id']
if response.get('version', None): # 'version' can be zero, need that case to pass test, so passing in None, not sure if this is best way.
self.shadow_version = response['version']
self.server_version = self.shadow_version
if response.get('name'):
self.shadow_name = response['name']
self.name = self.shadow_name
if response.get('tags'):
self.shadow_tags = response['tags']
self.tags = self.shadow_tags
if response.get('user_ids'):
self.shadow_user_ids = response['user_ids']
self.user_ids = self.shadow_user_ids
if response.get('content'):
self.shadow_content = response['content']
self.content = self.shadow_content
controller.updated_document(self)
if response.get('conflicts'):
return response['conflicts']
else:
return None
class DocumentController(object):
def __init__(self, rest_service):
self.documents = []
self.rest_service = rest_service
""" Get user visible documents, all locally stored documents that are not
scheduled for delete on the server (is_deleted_from_client).
"""
def client_visible_documents(self):
return filter(lambda document: not document.is_deleted_from_client, self.documents)
""" Create document from clients perspective. Sync will need to be performed to POST document
to server, or GET document state from server.
"""
def client_create_document(self, data=None):
document = Document(data)
self.documents.append(document)
logging.info("Created: %s", document)
return document
""" Delete document from clients perspective. If the document has never been synced to server
then delete immediatly. If it has been searched set is_deleted_from_client so that it will
no longer show up in client_visible_documents and so that it will be deleted from server on next sync.
"""
def client_delete_document(self, document):
if document.is_server_document():
document.is_deleted_from_client = True
logging.info("Scheduled Delete: %s", document)
else:
self.delete_document(document)
def updated_document(self, document):
logging.info("Updated: %s", document)
def delete_document(self, document):
self.documents.remove(document)
logging.info("Deleted: %s", document)
""" Sync client state with server. Note, server model doesn't handle many document sync requests at the same
time. So as in this example... each document sync needs to be performed synchronously. This will be fixed
on the server on a future date.
"""
def sync_documents(self):
syncing_documents = []
# 1. Get server documents index (id, version, name), mapped by id
server_documents_index_by_id = {}
for each_server_document_index in self.rest_service.GET_documents():
server_documents_index_by_id[each_server_document_index['id']] = each_server_document_index
# 2. Map local documents to server documents, and tag local documents that are deleted from server.
for each_document in self.documents:
if each_document.is_server_document():
each_server_document_index = server_documents_index_by_id.get(each_document.shadow_id)
if each_server_document_index:
each_document.server_version = each_server_document_index.get('version')
del server_documents_index_by_id[each_document.shadow_id]
each_document.is_deleted_from_server = False
else:
each_document.is_deleted_from_server = True
syncing_documents.append(each_document)
# 3. Create new local documents for server documents that don't map. (new ones)
for each_server_document_index_id in server_documents_index_by_id:
each_document = self.client_create_document(server_documents_index_by_id[each_server_document_index_id])
syncing_documents.append(each_document)
# 4. Sync each document
for each_document in syncing_documents:
conflicts = each_document.sync(self)
if conflicts:
logging.warn("%s conflicts: %s" % (each_document, conflicts))