def extract_post_1_to_subidea_1_1(request, participant2_user, reply_post_1, subidea_1_1, discussion, test_session): """ Links reply_post_1 to subidea_1_1 """ from assembl.models import Extract, Keyword from assembl.models.idea_content_link import ExtractNatureVocabulary, ExtractActionVocabulary e = Extract( body=u"body", creator=participant2_user, owner=participant2_user, content=reply_post_1, idea_id=subidea_1_1.id, # strange bug: Using idea directly fails discussion=discussion, extract_hash=u'extract_post_1_to_subidea_1_1', extract_nature=ExtractNatureVocabulary.Enum.actionable_solution, extract_action=ExtractActionVocabulary.Enum.give_examples) tags = Keyword.get_tags(['foo', 'bar'], discussion.id, test_session) e.tags = tags['new_tags'] + tags['tags'] test_session.add(e) test_session.flush() def fin(): print "finalizer extract_post_1_to_subidea_1_1" tags = e.tags e.tags = [] for tag in tags: test_session.delete(tag) test_session.delete(e) test_session.flush() request.addfinalizer(fin) return e
def test_extracts_on_post(admin_user, graphql_request, discussion, top_post_in_thread_phase): from graphene.relay import Node raw_id = int(Node.from_global_id(top_post_in_thread_phase)[1]) from assembl.models import Extract, Post post = Post.get(raw_id) post.extracts.append( Extract(body=u"super quote", important=False, creator=admin_user, owner=admin_user, discussion=discussion, extract_hash=u"extract1")) post.extracts.append( Extract(body=u"super important quote", important=True, creator=admin_user, owner=admin_user, discussion=discussion, extract_hash=u"extract2")) post.db.flush() res = schema.execute(u""" query Post($id: ID!) { post: node(id: $id) { ... on Post { extracts { body important } } } } """, context_value=graphql_request, variable_values={ "id": top_post_in_thread_phase, }) assert json.loads(json.dumps(res.data)) == { u'post': { u'extracts': [ { u'body': u'super quote', u'important': False }, { u'body': u'super important quote', u'important': True }, ] } }
def delete_extract(request): user_id = request.authenticated_userid discussion_id = int(request.matchdict['discussion_id']) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token(token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) if not (user_has_permission(discussion_id, user_id, P_EDIT_EXTRACT) or (user_has_permission(discussion_id, user_id, P_EDIT_MY_EXTRACT) and user_id == extract.owner_id)): raise HTTPForbidden() if not extract: return HTTPNoContent() # TODO: Tombstonable extracts??? extract.delete() return HTTPNoContent()
def extract_submitted_in_post_related_to_sub_idea_1_1_1( request, participant2_user, post_related_to_sub_idea_1_1_1, subidea_1_1, discussion, test_session): """ Create an extract in a post related to an idea.""" from assembl.models import Extract from assembl.models.idea_content_link import ExtractNatureVocabulary, ExtractActionVocabulary new_extract = Extract( discussion_id=discussion.id, body= u"Commodi maiores magni rerum. Sint natus corporis in qui in ut dignissimos cumque repellendus. Reprehenderit nihil illum.", creator=participant2_user, owner=participant2_user, content=post_related_to_sub_idea_1_1_1, extract_hash=u'extract_submitted_in_post_related_to_sub_idea_1_1_1', extract_nature=ExtractNatureVocabulary.Enum.actionable_solution, extract_action=ExtractActionVocabulary.Enum.give_examples) test_session.add(new_extract) test_session.flush() def fin(): print "finalizer extract_with_range_submitted_in_reply_post_1" test_session.delete(new_extract) test_session.flush() request.addfinalizer(fin) return new_extract
def delete_extract(request): user_id = authenticated_userid(request) discussion = request.context if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token(token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) permissions = get_permissions(user_id, discussion.id, extract) if P_EDIT_EXTRACT not in permissions: raise HTTPForbidden() if not extract: return HTTPNoContent() # TODO: Tombstonable extracts??? extract.delete() return HTTPNoContent()
def upgrade(pyramid_env): with context.begin_transaction(): op.create_table( 'text_fragment_identifier', sa.Column('id', sa.Integer, primary_key=True), sa.Column('extract_id', sa.Integer, sa.ForeignKey('extract.id')), sa.Column('xpath_start', sa.String), sa.Column('offset_start', sa.Integer), sa.Column('xpath_end', sa.String), sa.Column('offset_end', sa.Integer) ) op.add_column('extract', sa.Column('annotation_text', sa.UnicodeText)) # Do stuff with the app's models here. from assembl.models import Extract db = Extract.db() with transaction.manager: q = db.execute(''' SELECT extract.id, email.subject, email.body, post.id FROM extract JOIN email ON (email.id = extract.source_id) JOIN content ON (email.id = content.id) JOIN post ON (post.content_id = email.id) WHERE content.type = 'email' ''') vals = {ex_id: (sub, body, postid) for (ex_id, sub, body, postid) in q} for extract in db.query(Extract).options(lazyload('*')).all(): v = vals.get(extract.id) if v: tfi = extract._infer_text_fragment_inner(*v) if tfi: db.add(tfi)
def delete_extract(request): user_id = authenticated_userid(request) discussion_id = int(request.matchdict['discussion_id']) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token( token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) if not (user_has_permission(discussion_id, user_id, P_EDIT_EXTRACT) or (user_has_permission(discussion_id, user_id, P_EDIT_MY_EXTRACT) and user_id == extract.owner_id)): raise HTTPForbidden() if not extract: return HTTPNoContent() with transaction.manager: # TODO: Tombstonable extracts??? Extract.default_db.delete(extract) request.response.status = HTTPNoContent.code return HTTPNoContent()
def put_extract(request): """ Updating an Extract """ extract_id = request.matchdict['id'] user_id = authenticated_userid(request) updated_extract_data = json.loads(request.body) extract = Extract.get_instance(extract_id) if not extract: raise HTTPNotFound("Extract with id '%s' not found." % extract_id) extract.owner_id = user_id or get_database_id("User", extract.owner_id) extract.order = updated_extract_data.get('order', extract.order) idea_id = updated_extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if(idea.get_discussion_id() != extract.get_discussion_id()): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) extract.idea = idea else: extract.idea = None Extract.db.add(extract) #TODO: Merge ranges. Sigh. return {'ok': True}
def delete_extract(request): user_id = authenticated_userid(request) discussion_id = int(request.matchdict['discussion_id']) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token( token, request.registry.settings['session.secret']) if token: user_id = token['userId'] if not user_id: user_id = Everyone if not user_has_permission(discussion_id, user_id, P_DELETE_EXTRACT): return HTTPForbidden() extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) if not extract: return {'ok': False} with transaction.manager: Extract.db.delete(extract) return {'ok': True}
def extract_with_range_submitted_in_reply_post_1(request, discussion_admin_user, reply_post_1, subidea_1_1, discussion, test_session): """ Create an extract of a given range of text in a message """ from assembl.models import Extract, TextFragmentIdentifier, ExtractStates extract_body = "variable-temperature spectra indicate the onset of oxide-ion motion involving the interstitials at 130 °C, which is linked to an orthorhombic−tetragonal phase transition. For the V-doped phases, an oxide-ion conduction mechanism is observed that involves oxygen exchange between the Bi-O sublattice and rapidly rotating VO4 tetrahedral units. The more poorly conducting P-doped phase exhibits only vacancy conduction with no evidence of sublattice exchange, a result ascribed to the differing propensities of the dopants to undergo variable oxygen coordination. So I think it would be a very bad idea to allow hot beverages in coworking spaces." xpathStart = u"//div[@id='message-body-local:Content/%s']/" % reply_post_1.id xpathEnd = xpathStart offsetStart = 314 offsetEnd = 958 lang = 'en' extract_hash = Extract.get_extract_hash(lang, xpathStart, xpathEnd, offsetStart, offsetEnd, reply_post_1.id) new_extract = Extract(creator_id=discussion_admin_user.id, owner_id=discussion_admin_user.id, discussion_id=discussion.id, body=extract_body, important=True, content=reply_post_1, extract_state=ExtractStates.SUBMITTED.value, extract_hash=extract_hash) new_extract.lang = lang test_session.add(new_extract) new_range = TextFragmentIdentifier(extract=new_extract, xpath_start=xpathStart, offset_start=offsetStart, xpath_end=xpathEnd, offset_end=offsetEnd) test_session.add(new_range) test_session.flush() def fin(): print "finalizer extract_with_range_submitted_in_reply_post_1" test_session.delete(new_range) test_session.delete(new_extract) test_session.flush() request.addfinalizer(fin) return new_extract
def get_extract(request): extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) view_def = request.GET.get('view') or 'default' user_id = authenticated_userid(request) or Everyone permissions = request.permissions if extract is None: raise HTTPNotFound("Extract with id '%s' not found." % extract_id) return extract.generic_json(view_def, user_id, permissions)
def delete_extract(request): extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) if not extract: return {'ok': False} with transaction.manager: Extract.db.delete(extract) return {'ok': True}
def get_extract(request): extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) view_def = request.GET.get('view') if extract is None: raise HTTPNotFound( "Extract with id '%s' not found." % extract_id) if view_def: return extract.generic_json(view_def) else: return extract.serializable()
def get_extract(request): extract_id = request.matchdict['id'] extract = Extract.get_instance(extract_id) view_def = request.GET.get('view') or 'default' discussion_id = int(request.matchdict['discussion_id']) user_id = authenticated_userid(request) or Everyone permissions = get_permissions(user_id, discussion_id) if extract is None: raise HTTPNotFound( "Extract with id '%s' not found." % extract_id) return extract.generic_json(view_def, user_id, permissions)
def put_extract(request): """ Updating an Extract """ extract_id = request.matchdict['id'] user_id = authenticated_userid(request) discussion = request.context if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token(token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone extract = Extract.get_instance(extract_id) if not extract: raise HTTPNotFound("Extract with id '%s' not found." % extract_id) permissions = get_permissions(user_id, discussion.id, extract) if P_EDIT_EXTRACT not in permissions: raise HTTPForbidden() updated_extract_data = json.loads(request.body) extract.owner_id = user_id or AgentProfile.get_database_id( extract.owner_id) extract.order = updated_extract_data.get('order', extract.order) extract.important = updated_extract_data.get('important', extract.important) idea_id = updated_extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if (idea.discussion != extract.discussion): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) if not idea.has_permission_req(P_ASSOCIATE_EXTRACT): raise HTTPForbidden("Cannot associate extact with this idea") extract.idea = idea else: extract.idea = None Extract.default_db.add(extract) #TODO: Merge ranges. Sigh. return {'ok': True}
def put_extract(request): """ Updating an Extract """ extract_id = request.matchdict['id'] user_id = request.authenticated_userid discussion_id = int(request.matchdict['discussion_id']) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token(token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone updated_extract_data = json.loads(request.body) extract = Extract.get_instance(extract_id) if not extract: raise HTTPNotFound("Extract with id '%s' not found." % extract_id) if not (user_has_permission(discussion_id, user_id, P_EDIT_EXTRACT) or (user_has_permission(discussion_id, user_id, P_EDIT_MY_EXTRACT) and user_id == extract.owner_id)): return HTTPForbidden() extract.owner_id = user_id or get_database_id("User", extract.owner_id) extract.order = updated_extract_data.get('order', extract.order) extract.important = updated_extract_data.get('important', extract.important) idea_id = updated_extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if (idea.discussion != extract.discussion): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) extract.idea = idea else: extract.idea = None Extract.default_db.add(extract) #TODO: Merge ranges. Sigh. return {'ok': True}
def put_extract(request): """ Updating an Extract """ extract_id = request.matchdict['id'] user_id = authenticated_userid(request) discussion_id = int(request.matchdict['discussion_id']) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token( token, request.registry.settings['session.secret']) if token: user_id = token['userId'] if not user_id: user_id = Everyone updated_extract_data = json.loads(request.body) extract = Extract.get_instance(extract_id) if not extract: raise HTTPNotFound("Extract with id '%s' not found." % extract_id) if not (user_has_permission(discussion_id, user_id, P_EDIT_EXTRACT) or (user_has_permission(discussion_id, user_id, P_EDIT_MY_EXTRACT) and user_id == extract.owner_id)): return HTTPForbidden() extract.owner_id = user_id or get_database_id("User", extract.owner_id) extract.order = updated_extract_data.get('order', extract.order) extract.important = updated_extract_data.get('important', extract.important) idea_id = updated_extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if(idea.discussion != extract.discussion): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) extract.idea = idea else: extract.idea = None Extract.db.add(extract) #TODO: Merge ranges. Sigh. return {'ok': True}
def upgrade(pyramid_env): from assembl.models import Extract, TextFragmentIdentifier, Content, Post db = Extract.db() reg = re.compile(r"^//div\[@id='message-([0-9]+)'\](.*)") with transaction.manager: db.query(TextFragmentIdentifier).filter_by(extract=None).delete() for tfi in db.query(TextFragmentIdentifier).join( Extract, Content, Post).all(): xpo = tfi.xpath_start print xpo match = reg.match(xpo) if match: id, remainder = match.groups() uri = Post.uri_generic(id) xp = "//div[@id='message-%s']%s" % ( uri, remainder) print xp tfi.xpath_start = tfi.xpath_end = xp
def _get_extracts_real(request, view_def='default', ids=None, user_id=None): discussion = request.discussion user_id = user_id or Everyone all_extracts = discussion.db.query(Extract).filter( Extract.discussion_id == discussion.id) if ids: ids = [Extract.get_database_id(id) for id in ids] all_extracts = all_extracts.filter(Extract.id.in_(ids)) all_extracts = all_extracts.options(joinedload_all(Extract.content)) all_extracts = all_extracts.options( joinedload_all(Extract.selectors).joinedload( AnnotationSelector.extract, innerjoin=True)) permissions = request.permissions return [ extract.generic_json(view_def, user_id, permissions) for extract in all_extracts ]
def extract_post_1_to_subidea_1_1( request, participant2_user, reply_post_1, subidea_1_1, discussion, test_session): """ Links reply_post_1 to subidea_1_1 """ from assembl.models import Extract e = Extract( body=u"body", creator=participant2_user, owner=participant2_user, content=reply_post_1, idea_id=subidea_1_1.id, # strange bug: Using idea directly fails discussion=discussion) test_session.add(e) test_session.flush() def fin(): print "finalizer extract_post_1_to_subidea_1_1" test_session.delete(e) test_session.flush() request.addfinalizer(fin) return e
def upgrade(pyramid_env): from assembl.models import Extract, Mailbox db = Mailbox.db() with transaction.manager: for mb in db.query(Mailbox).all(): Mailbox.reprocess_content(mb) db = Extract.db() with transaction.manager: q = db.execute(''' SELECT extract.id, email.subject, email.body, post.id FROM extract JOIN email ON (email.id = extract.source_id) JOIN content ON (email.id = content.id) JOIN post ON (post.content_id = email.id) WHERE content.type = 'email' ''') vals = {ex_id: (sub, body, postid) for (ex_id, sub, body, postid) in q} for extract in db.query(Extract).options(lazyload('*')).all(): v = vals.get(extract.id) if v: tfi = extract._infer_text_fragment_inner(*v) if tfi: db.add(tfi)
def post_extract(request): """ Create a new extract. """ extract_data = json.loads(request.body) discussion_id = int(request.matchdict['discussion_id']) user_id = request.authenticated_userid if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token( token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone if not user_has_permission(discussion_id, user_id, P_ADD_EXTRACT): #TODO: maparent: restore this code once it works: #return HTTPForbidden(result=ACLDenied(permission=P_ADD_EXTRACT)) return HTTPForbidden() if not user_id or user_id == Everyone: # TODO: Create an anonymous user. raise HTTPServerError("Anonymous extracts are not implemeted yet.") content = None uri = extract_data.get('uri') important = extract_data.get('important', False) annotation_text = None if uri: # Straight from annotator annotation_text = extract_data.get('text') else: target = extract_data.get('target') if not (target or uri): raise HTTPBadRequest("No target") target_class = sqla.get_named_class(target.get('@type')) if issubclass(target_class, Post): post_id = target.get('@id') post = Post.get_instance(post_id) if not post: raise HTTPNotFound( "Post with id '%s' not found." % post_id) content = post elif issubclass(target_class, Webpage): uri = target.get('url') if uri and not content: content = Webpage.get_instance(uri) if not content: # TODO: maparent: This is actually a singleton pattern, should be # handled by the AnnotatorSource now that it exists... source = AnnotatorSource.default_db.query(AnnotatorSource).filter_by( discussion_id=discussion_id).filter( cast(AnnotatorSource.name, Unicode) == 'Annotator').first() if not source: source = AnnotatorSource( name='Annotator', discussion_id=discussion_id) content = Webpage(url=uri, discussion_id=discussion_id) extract_body = extract_data.get('quote', '') idea_id = extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if(idea.discussion.id != discussion_id): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) else: idea = None ranges = extract_data.get('ranges', []) extract_hash = Extract.get_extract_hash( None, u"".join([r['start'] for r in ranges]), u"".join([r['end'] for r in ranges]), u"".join([r['startOffset'] for r in ranges]), u"".join([r['endOffset'] for r in ranges]), content.id ) new_extract = Extract( creator_id=user_id, owner_id=user_id, discussion_id=discussion_id, body=extract_body, idea=idea, important=important, annotation_text=annotation_text, content=content, extract_hash=extract_hash ) Extract.default_db.add(new_extract) for range_data in ranges: range = TextFragmentIdentifier( extract=new_extract, xpath_start=range_data['start'], offset_start=range_data['startOffset'], xpath_end=range_data['end'], offset_end=range_data['endOffset']) TextFragmentIdentifier.default_db.add(range) Extract.default_db.flush() return {'ok': True, '@id': new_extract.uri()}
def post_extract(request): """ Create a new extract. """ extract_data = json.loads(request.body) discussion = request.context db = discussion.db user_id = authenticated_userid(request) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token(token, request.registry.settings['session.secret']) if token: user_id = token['userId'] user_id = user_id or Everyone permissions = get_permissions(user_id, discussion_id) else: permissions = request.permissions if P_ADD_EXTRACT not in permissions: #TODO: maparent: restore this code once it works: #raise HTTPForbidden(result=ACLDenied(permission=P_ADD_EXTRACT)) raise HTTPForbidden() if not user_id or user_id == Everyone: # TODO: Create an anonymous user. raise HTTPServerError("Anonymous extracts are not implemeted yet.") content = None uri = extract_data.get('uri') important = extract_data.get('important', False) annotation_text = extract_data.get('text') target = extract_data.get('target') if not uri: # Extract from an internal post if not target: raise HTTPBadRequest("No target") target_class = sqla.get_named_class(target.get('@type')) if issubclass(target_class, Post): post_id = target.get('@id') post = Post.get_instance(post_id) if not post: raise HTTPNotFound("Post with id '%s' not found." % post_id) content = post elif issubclass(target_class, Webpage): uri = target.get('url') if uri and not content: content = Webpage.get_instance(uri) if not content: # TODO: maparent: This is actually a singleton pattern, should be # handled by the AnnotatorSource now that it exists... source = db.query(AnnotatorSource).filter_by( discussion=discussion).filter( cast(AnnotatorSource.name, Unicode) == 'Annotator').first() if not source: source = AnnotatorSource(name='Annotator', discussion=discussion) db.add(source) content = Webpage(url=uri, discussion=discussion) db.add(content) extract_body = extract_data.get('quote', None) idea_id = extract_data.get('idIdea', None) if idea_id: idea = Idea.get_instance(idea_id) if (idea.discussion.id != discussion.id): raise HTTPBadRequest( "Extract from discussion %s cannot be associated with an idea from a different discussion." % extract.get_discussion_id()) if not idea.has_permission_req(P_ASSOCIATE_EXTRACT): raise HTTPForbidden("Cannot associate extact with this idea") else: idea = None new_extract = Extract(creator_id=user_id, owner_id=user_id, discussion=discussion, idea=idea, important=important, annotation_text=annotation_text, content=content) db.add(new_extract) for range_data in extract_data.get('ranges', []): range = TextFragmentIdentifier(extract=new_extract, body=extract_body, xpath_start=range_data['start'], offset_start=range_data['startOffset'], xpath_end=range_data['end'], offset_end=range_data['endOffset']) db.add(range) db.flush() return {'ok': True, '@id': new_extract.uri()}
def post_extract(request): """ Create a new extract. """ extract_data = json.loads(request.body) discussion_id = int(request.matchdict['discussion_id']) user_id = authenticated_userid(request) if not user_id: # Straight from annotator token = request.headers.get('X-Annotator-Auth-Token') if token: token = decode_token( token, request.registry.settings['session.secret']) if token: user_id = token['userId'] if not user_id: user_id = Everyone if not user_has_permission(discussion_id, user_id, P_ADD_EXTRACT): #TODO: maparent: restore this code once it works: #return HTTPForbidden(result=ACLDenied(permission=P_ADD_EXTRACT)) return HTTPForbidden() if user_id == Everyone: # TODO: Create an anonymous user. raise HTTPServerError("Anonymous extracts are not implemeted yet.") content = None uri = extract_data.get('uri') annotation_text = None if uri: # Straight from annotator annotation_text = extract_data.get('text') else: target = extract_data.get('target') if not (target or uri): raise HTTPClientError("No target") target_type = target.get('@type') if target_type == 'email': post_id = target.get('@id') post = Post.get_instance(post_id) if not post: raise HTTPNotFound( "Post with id '%s' not found." % post_id) content = post elif target_type == 'webpage': uri = target.get('url') if uri and not content: content = Webpage.get_instance(uri) if not content: # TODO: maparent: This is actually a singleton pattern, should be # handled by the AnnotatorSource now that it exists... source = AnnotatorSource.db.query(AnnotatorSource).filter_by( discussion_id=discussion_id).filter( cast(AnnotatorSource.name, Unicode) == 'Annotator').first() if not source: source = AnnotatorSource( name='Annotator', discussion_id=discussion_id, type='source') content = Webpage(url=uri, discussion_id=discussion_id) extract_body = extract_data.get('quote', '') new_extract = Extract( creator_id=user_id, owner_id=user_id, discussion_id=discussion_id, body=extract_body, annotation_text=annotation_text, content=content ) Extract.db.add(new_extract) for range_data in extract_data.get('ranges', []): range = TextFragmentIdentifier( extract=new_extract, xpath_start=range_data['start'], offset_start=range_data['startOffset'], xpath_end=range_data['end'], offset_end=range_data['endOffset']) TextFragmentIdentifier.db.add(range) Extract.db.flush() return {'ok': True, 'id': new_extract.uri()}