Example #1
0
def widget_view(request):
    # IF_OWNED not applicable for widgets... so far
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    check_permissions(ctx, user_id, permissions, CrudPermissions.READ)
    view = (request.matchdict or {}).get('view', None)\
        or ctx.get_default_view() or 'default'
    json = ctx._instance.generic_json(view, user_id, permissions)
    # json['discussion'] = ...
    if user_id != Everyone:
        user = User.get(user_id)
        user_state = ctx._instance.get_user_state(user_id)
        json['user'] = user.generic_json(view, user_id, permissions)
        json['user_permissions'] = get_permissions(
            user_id, ctx._instance.get_discussion_id())
        if user_state is not None:
            json['user_state'] = user_state
    target_id = request.GET.get('target', None)
    if target_id:
        idea = Idea.get_instance(target_id)
        if idea:
            json['target'] = idea.generic_json(view, user_id, permissions)
        else:
            return HTTPNotFound("No idea "+target_id)
    return json
Example #2
0
def voting_widget_view(request):
    user_id = authenticated_userid(request) or Everyone
    ctx = request.context
    view = (request.matchdict or {}).get('view', None)\
        or ctx.get_default_view() or 'default'
    widget = ctx._instance
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    json = widget.generic_json(view, user_id, permissions)
    #json['discussion'] = ...
    if user_id != Everyone:
        user = User.get(user_id)
        json['user'] = user.generic_json(view, user_id, permissions)
        json['user_permissions'] = get_permissions(
            user_id, widget.get_discussion_id())
        user_state = widget.get_user_state(user_id)
        if user_state is not None:
            json['user_state'] = user_state
    target_id = request.GET.get('target', None)
    if target_id and Idea.get_database_id(target_id):
        json['user_votes_url'] = widget.get_user_votes_url(target_id)
        json['voting_urls'] = widget.get_voting_urls(target_id)
    json['criteria'] = [idea.generic_json(view, user_id, permissions)
                        for idea in widget.criteria]
    return json
Example #3
0
def add_local_role(request):
    # Do not use check_permissions, this is a special case
    ctx = request.context
    user_id = request.authenticated_userid
    if not user_id:
        raise HTTPUnauthorized()
    discussion_id = ctx.get_discussion_id()
    discussion = Discussion.get(discussion_id)
    user_uri = User.uri_generic(user_id)
    if discussion_id is None:
        raise HTTPBadRequest()
    permissions = get_permissions(user_id, discussion_id)
    json = request.json_body
    if "discussion" not in json:
        json["discussion"] = Discussion.uri_generic(discussion_id)
    requested_user = json.get('user', None)
    if not requested_user:
        json['user'] = requested_user = user_uri
    elif requested_user != user_uri and P_ADMIN_DISC not in permissions:
        raise HTTPUnauthorized()
    if P_ADMIN_DISC not in permissions:
        if P_SELF_REGISTER in permissions:
            json['requested'] = False
            json['role'] = R_PARTICIPANT
            req_user = User.get_instance(requested_user)
            if not discussion.check_authorized_email(req_user):
                raise HTTPForbidden()
        elif P_SELF_REGISTER_REQUEST in permissions:
            json['requested'] = True
        else:
            raise HTTPUnauthorized()
    try:
        instances = ctx.create_object("LocalUserRole", json, user_id)
    except HTTPClientError as e:
        raise e
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db
        for instance in instances:
            db.add(instance)
        db.flush()
        # Side effect: materialize subscriptions.
        if not first.requested:
            # relationship may not be initialized
            user = first.user or User.get(first.user_id)
            user.get_notification_subscriptions(discussion_id, True)

        # Update the user's AgentStatusInDiscussion
        user.update_agent_status_subscribe(discussion)

        view = request.GET.get('view', None) or 'default'
        permissions = get_permissions(
            user_id, ctx.get_discussion_id())
        return CreationResponse(first, user_id, permissions, view)
Example #4
0
def add_local_role(request):
    # Do not use check_permissions, this is a special case
    ctx = request.context
    user_id = authenticated_userid(request)
    if user_id == Everyone:
        raise HTTPUnauthorized()
    discussion_id = ctx.get_discussion_id()
    user_uri = User.uri_generic(user_id)
    if discussion_id is None:
        raise HTTPBadRequest()
    permissions = get_permissions(user_id, discussion_id)
    json = request.json_body
    if "discussion" not in json:
        json["discussion"] = Discussion.uri_generic(discussion_id)
    requested_user = json.get('user', None)
    if not requested_user:
        json['user'] = requested_user = user_uri
    elif requested_user != user_uri and P_ADMIN_DISC not in permissions:
        raise HTTPUnauthorized()
    if P_ADMIN_DISC not in permissions:
        if P_SELF_REGISTER in permissions:
            json['requested'] = False
            json['role'] = R_PARTICIPANT
        elif P_SELF_REGISTER_REQUEST in permissions:
            json['requested'] = True
        else:
            raise HTTPUnauthorized()
    try:
        instances = ctx.create_object("LocalUserRole", json, user_id)
    except HTTPClientError as e:
        raise e
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db()
        for instance in instances:
            db.add(instance)
        db.flush()
        # Side effect: materialize subscriptions.
        if not first.requested:
            # relationship may not be initialized
            user = first.user or User.get(first.user_id)
            user.get_notification_subscriptions(discussion_id, True)
        view = request.GET.get('view', None) or 'default'
        permissions = get_permissions(
            user_id, ctx.get_discussion_id())
        return Response(
            dumps(first.generic_json(view, user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
Example #5
0
def discussion_edit(request):
    discussion_id = int(request.matchdict["discussion_id"])
    discussion = Discussion.get_instance(discussion_id)
    user_id = authenticated_userid(request)
    assert user_id
    permissions = get_permissions(user_id, discussion_id)
    partners = json.dumps(
        [p.generic_json(user_id=user_id, permissions=permissions) for p in discussion.partner_organizations]
    )

    if not discussion:
        raise HTTPNotFound("Discussion with id '%d' not found." % (discussion_id,))

    context = dict(
        get_default_context(request),
        discussion=discussion,
        admin_discussion_permissions_url=request.route_url("discussion_permissions", discussion_id=discussion.id),
        partners=partners,
    )

    if request.method == "POST":

        g = lambda x: request.POST.get(x, None)

        (topic, slug, objectives) = (g("topic"), g("slug"), g("objectives"))

        discussion.topic = topic
        discussion.slug = slug
        discussion.objectives = objectives

    return render_to_response("admin/discussion_edit.jinja2", context, request=request)
Example #6
0
def votes_collection_add(request):
    ctx = request.context
    user_id = authenticated_userid(request)
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    check_permissions(ctx, user_id, permissions, CrudPermissions.CREATE)
    args = request.params
    if 'type' in args:
        args = dict(args)
        typename = args['type']
        del args['type']
    else:
        typename = ctx.collection_class.external_typename()
    args['voter_id'] = user_id
    try:
        instances = ctx.create_object(typename, None, user_id, **args)
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db()
        for instance in instances:
            db.add(instance)
        print "before flush"
        db.flush()
        print "after flush"
        return Response(
            dumps(first.generic_json('default', user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
    raise HTTPBadRequest()
Example #7
0
def save_synthesis(request):
    synthesis_id = request.matchdict['id']
    discussion_id = int(request.matchdict['discussion_id'])
    if synthesis_id == 'next_synthesis':
        discussion = Discussion.get_instance(discussion_id)
        synthesis = discussion.get_next_synthesis()
    else:
        synthesis = Synthesis.get_instance(synthesis_id)
    if not synthesis:
        raise HTTPBadRequest("Synthesis with id '%s' not found." % synthesis_id)

    synthesis_data = json.loads(request.body)
    user_id = request.authenticated_userid
    permissions = get_permissions(user_id, discussion_id)

    for key in ('subject', 'introduction', 'conclusion'):
        if key in synthesis_data:
            ls_data = synthesis_data[key]
            if ls_data is None:
                continue
            assert isinstance(ls_data, dict)
            current = LangString.create_from_json(
                ls_data, user_id, permissions=permissions)
            setattr(synthesis, key, current)

    Synthesis.default_db.add(synthesis)
    Synthesis.default_db.flush()

    return {'ok': True, 'id': synthesis.uri()}
Example #8
0
    def mutate(root, args, context, info):
        model = models.User
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        global_id = args.get('id')
        id_ = int(Node.from_global_id(global_id)[1])
        user = model.get(id_)
        # Global permission check. Permission is checked in the preferences setter
        # Is it necessary?
        permissions = get_permissions(user_id, discussion_id)
        allowed = user.user_can(
            user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized("You don't have the authorization to update this haversting locale. If you think it's an error, please reconnect to assembl.")

        with model.default_db.no_autoflush as db:
            preferences = user.get_preferences_for_discussion(discussion)
            # Permission check in the preferences setter
            # See models.user_key_value and models.preferences preference_data_list
            preferences['harvesting_translation'] = args.get('translation')
            db.flush()

        return UpdateHarvestingTranslationPreference(
            preferences=preferences)
Example #9
0
    def mutate(root, args, context, info):
        user_id = context.authenticated_userid or Everyone
        post_id = args.get('post_id')
        post_id = int(Node.from_global_id(post_id)[1])
        post = models.Post.get(post_id)
        discussion_id = context.matchdict['discussion_id']
        permissions = get_permissions(user_id, discussion_id)

        require_instance_permission(CrudPermissions.DELETE, post, context)

        # Same logic as in assembl/views/api2/post.py:delete_post_instance
        # Remove extracts associated to this post
        extracts_to_remove = post.db.query(models.Extract).filter(
            models.Extract.content_id == post.id).all()
        for extract in extracts_to_remove:
            extract.delete()

        if user_id == post.creator_id and P_DELETE_MY_POST in permissions:
            cause = models.PublicationStates.DELETED_BY_USER
        elif P_DELETE_POST in permissions:
            cause = models.PublicationStates.DELETED_BY_ADMIN

        post.delete_post(cause)
        post.db.flush()
        return DeletePost(post=post)
Example #10
0
def require_instance_permission(permission_type, instance, request):
    user_id = request.authenticated_userid or Everyone
    discussion_id = request.matchdict['discussion_id']
    permissions = get_permissions(user_id, discussion_id)
    allowed = instance.user_can(user_id, permission_type, permissions) if instance else False
    if not allowed:
        raise HTTPUnauthorized(request.localizer.translate(error))
Example #11
0
def require_cls_permission(permission_type, cls, request):
    user_id = request.authenticated_userid or Everyone
    discussion_id = request.matchdict['discussion_id']
    permissions = get_permissions(user_id, discussion_id)
    allowed = cls.user_can_cls(user_id, permission_type, permissions)
    if not allowed or (allowed == IF_OWNED and user_id == Everyone):
        raise HTTPUnauthorized(request.localizer.translate(error))
Example #12
0
def get_token(request):
    user_id = authenticated_userid(request)
    discussion_id = request.context.get_discussion_id()
    if not user_id:
        raise HTTPUnauthorized()
    req_permissions = request.GET.getall('permission') or [
        P_READ, P_READ_PUBLIC_CIF]
    random_seed = request.GET.get('seed', None)
    if random_seed:
        # We need some determinism
        import random
        random.seed(random_seed)
        random_str = ''.join([chr(random.randint(0,256)) for i in range(8)])
        random.seed(urandom(8))
    else:
        random_str = urandom(8)
    if isinstance(req_permissions, list):
        req_permissions = set(req_permissions)
    else:
        req_permissions = set((req_permissions,))
    permissions = set(get_permissions(user_id, discussion_id))
    if not req_permissions:
        req_permissions = permissions
    else:
        if P_READ in permissions:
            permissions.add(P_READ_PUBLIC_CIF)
        if P_SYSADMIN not in permissions:
            req_permissions = list(req_permissions.intersection(permissions))
    req_permissions = list(req_permissions)
    data = [str(user_id), str(discussion_id)]
    data.extend([str(x) for (x,) in Permission.db.query(Permission.id).filter(
        Permission.name.in_(req_permissions)).all()])
    data = ','.join(data) + '.' + base64.urlsafe_b64encode(random_str)
    return Response(body=data_token(data), content_type="text/text")
Example #13
0
 def resolve_email(self, args, context, info):
     user_id = context.authenticated_userid or Everyone
     discussion_id = context.matchdict['discussion_id']
     permissions = get_permissions(user_id, discussion_id)
     include_emails = P_ADMIN_DISC in permissions or P_SYSADMIN in permissions
     if include_emails or self.id == user_id:
         return self.get_preferred_email()
Example #14
0
def notif_collection_add_json(request):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    check_permissions(ctx, user_id, permissions, CrudPermissions.CREATE)
    typename = ctx.collection_class.external_typename()
    typename = request.json_body.get(
        '@type', ctx.collection_class.external_typename())
    json = request.json_body
    try:
        instances = ctx.create_object(typename, json, user_id)
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db
        for instance in instances:
            db.add(instance)
        db.flush()
        templates = ctx.find_collection('Discussion.user_templates')
        if templates:
            templates.parent_instance.reset_participant_default_subscriptions(False)
        view = request.GET.get('view', None) or 'default'
        return Response(
            dumps(first.generic_json(view, user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
Example #15
0
def create_idea(request):
    discussion_id = int(request.matchdict['discussion_id'])
    session = Discussion.default_db
    discussion = session.query(Discussion).get(int(discussion_id))
    user_id = request.authenticated_userid
    permissions = get_permissions(user_id, discussion.id)
    idea_data = json.loads(request.body)
    kwargs = {
        "discussion": discussion
    }

    for key, attr_name in langstring_fields.iteritems():
        if key in idea_data:
            ls_data = idea_data[key]
            if ls_data is None:
                continue
            assert isinstance(ls_data, dict)
            current = LangString.create_from_json(
                ls_data, user_id, permissions=permissions)
            kwargs[attr_name] = current

    new_idea = Idea(**kwargs)

    session.add(new_idea)

    if idea_data['parentId']:
        parent = Idea.get_instance(idea_data['parentId'])
    else:
        parent = discussion.root_idea
    session.add(IdeaLink(source=parent, target=new_idea, order=idea_data.get('order', 0.0)))

    session.flush()

    return {'ok': True, '@id': new_idea.uri()}
Example #16
0
def _get_ideas_real(discussion, view_def=None, ids=None, user_id=None):
    user_id = user_id or Everyone
    # optimization: Recursive widget links.
    from assembl.models import (
        Widget, IdeaWidgetLink, IdeaDescendantsShowingWidgetLink)
    universal_widget_links = []
    by_idea_widget_links = defaultdict(list)
    widget_links = discussion.db.query(IdeaWidgetLink
        ).join(Widget).join(Discussion).filter(
        Widget.test_active(), Discussion.id == discussion.id,
        IdeaDescendantsShowingWidgetLink.polymorphic_filter()
        ).options(joinedload_all(IdeaWidgetLink.idea)).all()
    for wlink in widget_links:
        if isinstance(wlink.idea, RootIdea):
            universal_widget_links.append({
                '@type': wlink.external_typename(),
                'widget': Widget.uri_generic(wlink.widget_id)})
        else:
            for id in wlink.idea.get_all_descendants(True):
                by_idea_widget_links[Idea.uri_generic(id)].append({
                    '@type': wlink.external_typename(),
                    'widget': Widget.uri_generic(wlink.widget_id)})

    next_synthesis = discussion.get_next_synthesis()
    ideas = discussion.db.query(Idea).filter_by(
        discussion_id=discussion.id
    )

    ideas = ideas.outerjoin(SubGraphIdeaAssociation,
                    and_(SubGraphIdeaAssociation.sub_graph_id==next_synthesis.id, SubGraphIdeaAssociation.idea_id==Idea.id)
        )
    
    ideas = ideas.outerjoin(IdeaLink,
                    and_(IdeaLink.target_id==Idea.id)
        )
    
    ideas = ideas.order_by(IdeaLink.order, Idea.creation_date)
    
    if ids:
        ids = [get_database_id("Idea", id) for id in ids]
        ideas = ideas.filter(Idea.id.in_(ids))
    # remove tombstones
    ideas = ideas.filter(and_(*Idea.base_conditions()))
    ideas = ideas.options(
        joinedload_all(Idea.source_links),
        joinedload_all(Idea.has_showing_widget_links),
        undefer(Idea.num_children))

    permissions = get_permissions(user_id, discussion.id)
    Idea.prepare_counters(discussion.id, True)
    retval = [idea.generic_json(view_def, user_id, permissions)
              for idea in ideas]
    retval = [x for x in retval if x is not None]
    for r in retval:
        if r.get('widget_links', None) is not None:
            links = r['widget_links'][:]
            links.extend(universal_widget_links)
            links.extend(by_idea_widget_links[r['@id']])
            r['active_widget_links'] = links
    return retval
Example #17
0
 def create_object(self, typename=None, json=None, user_id=None, **kwargs):
     cls = self.get_collection_class(typename)
     permissions = get_permissions(
         user_id, self.get_discussion_id())
     permissions.extend(self.ctx_permissions(permissions))
     with self.parent_instance.db.no_autoflush:
         try:
             if json is None:
                 mapper = sqlainspect(cls)
                 for prop in ('creator_id', 'user_id'):
                     if prop in mapper.c and prop not in kwargs:
                         kwargs[prop] = user_id
                         break
                 inst = cls(**dict(process_args(kwargs, cls)))
             else:
                 inst = cls.create_from_json(
                     json, user_id, self, permissions=permissions)
                 kwargs.update(json)
         except Exception as e:
             print_exc()
             raise e
         assocs = [inst]
         self.decorate_instance(inst, assocs, user_id, self, kwargs)
         if json is None:
             inst = inst.handle_duplication(
                 None, None, self, permissions, user_id,
                 None, None, kwargs)
             assocs[0] = inst
     return assocs
Example #18
0
def delete_post_instance(request):
    # Users who are allowed to delete (actually tombstone) a Post instance:
    # - user who is the author of the Post instance and who has the P_DELETE_MY_POST permission in this discussion
    # - user who has the P_DELETE_POST permission in this discussion
    ctx = request.context
    user_id = request.authenticated_userid or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    instance = ctx._instance

    allowed = False
    if (user_id == instance.creator_id and P_DELETE_MY_POST in permissions) or (P_DELETE_POST in permissions):
        allowed = True
    if not allowed:
        raise HTTPUnauthorized()

    # Remove extracts associated to this post
    extracts_to_remove = instance.db.query(Extract).filter(Extract.content_id == instance.id).all()
    number_of_extracts = len(extracts_to_remove)
    for extract in extracts_to_remove:
        extract.delete()

    if user_id == instance.creator_id and P_DELETE_MY_POST in permissions:
        cause = PublicationStates.DELETED_BY_USER
    elif P_DELETE_POST in permissions:
        cause = PublicationStates.DELETED_BY_ADMIN

    instance.delete_post(cause)

    return {
        "result": "Post has been successfully deleted.",
        "removed_extracts": number_of_extracts
    }
Example #19
0
def set_local_role(request):
    # Do not use check_permissions, this is a special case
    ctx = request.context
    instance = ctx._instance
    user_id = authenticated_userid(request)
    if not user_id:
        raise HTTPUnauthorized()
    discussion_id = ctx.get_discussion_id()
    user_uri = User.uri_generic(user_id)
    if discussion_id is None:
        raise HTTPBadRequest()
    permissions = get_permissions(user_id, discussion_id)
    json = request.json_body
    requested_user = json.get('user', None)
    if not requested_user:
        json['user'] = requested_user = user_uri
    elif requested_user != user_uri and P_ADMIN_DISC not in permissions:
        raise HTTPUnauthorized()
    if P_ADMIN_DISC not in permissions:
        if P_SELF_REGISTER in permissions:
            json['requested'] = False
            json['role'] = R_PARTICIPANT
        elif P_SELF_REGISTER_REQUEST in permissions:
            json['requested'] = True
        else:
            raise HTTPUnauthorized()
    updated = instance.update_from_json(json, user_id, ctx)
    view = request.GET.get('view', None) or 'default'
    if view == 'id_only':
        return [updated.uri()]
    else:
        return updated.generic_json(view, user_id, permissions)
Example #20
0
def instance_view_jsonld(request):
    from assembl.semantic.virtuoso_mapping import AssemblQuadStorageManager
    from rdflib import URIRef, ConjunctiveGraph
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    instance = ctx._instance
    if not instance.user_can(user_id, CrudPermissions.READ, permissions):
        return HTTPUnauthorized()
    discussion = ctx.get_instance_of_class(Discussion)
    if not discussion:
        raise HTTPNotFound()
    aqsm = AssemblQuadStorageManager()
    uri = URIRef(aqsm.local_uri() + instance.uri()[6:])
    d_storage_name = aqsm.discussion_storage_name(discussion.id)
    v = get_virtuoso(instance.db, d_storage_name)
    cg = ConjunctiveGraph(v, d_storage_name)
    result = cg.triples((uri, None, None))
    #result = v.query('select ?p ?o ?g where {graph ?g {<%s> ?p ?o}}' % uri)
    # Something is wrong here.
    triples = '\n'.join([
        '%s %s %s.' % (uri.n3(), p.n3(), o.n3())
        for (s, p, o) in result
        if '_with_no_name_entry' not in o])
    return aqsm.quads_to_jsonld(triples)
Example #21
0
def collection_add(request, args):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    check_permissions(ctx, user_id, permissions, CrudPermissions.CREATE)
    if 'type' in args:
        args = dict(args)
        typename = args['type']
        del args['type']
    else:
        typename = ctx.collection_class.external_typename()
    session = User.default_db
    old_autoflush = session.autoflush
    session.autoflush = False
    try:
        instances = ctx.create_object(typename, None, user_id, **args)
    except Exception as e:
        session.autoflush = old_autoflush
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db
        for instance in instances:
            db.add(instance)
        session.autoflush = old_autoflush
        session.flush()
        return Response(
            dumps(first.generic_json('default', user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
    raise HTTPBadRequest()
Example #22
0
def class_add(request):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    check_permissions(ctx, user_id, permissions, CrudPermissions.CREATE)
    args = request.params
    typename = args.get('type', None)
    if typename:
        cls = ctx.get_class(typename)
    else:
        cls = request.context._class
        typename = cls.external_typename()
    try:
        instances = ctx.create_object(typename, None, user_id, **args)
    except ObjectNotUniqueError as e:
        raise JSONError(409, str(e))
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db
        for instance in instances:
            db.add(instance)
        db.flush()
        return Response(
            dumps(first.generic_json('default', user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
    raise HTTPBadRequest()
Example #23
0
def fetch_posts(request):
    ctx = request.context
    csource = ctx._instance
    force_restart = request.params.get('force_restart', False)
    reimport = request.params.get('reimport', False)
    limit = request.params.get('limit', None)
    if limit:
        try:
            limit = int(limit)
        except ValueError:
            raise HTTPBadRequest("Non-numeric limit value: "+limit)
    if force_restart or reimport or limit:
        # Only discussion admins
        user_id = authenticated_userid(request) or Everyone
        permissions = get_permissions(
            user_id, ctx.get_discussion_id())
        if P_ADMIN_DISC not in permissions:
            requested = []
            if reimport:
                requested.append('reimport')
            if force_restart:
                requested.append('force restart')
            raise HTTPUnauthorized("Only discussion administrator can "+'and'.join(requested))

    wake(csource.id, reimport, force_restart, limit=limit)
    return {"message": "Source notified",
            "name": csource.name}
Example #24
0
def collection_add_json(request, json=None):
    ctx = request.context
    json = request.json_body if json is None else json
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    typename = ctx.collection_class.external_typename()
    typename = json.get(
        '@type', ctx.collection_class.external_typename())
    cls = ctx.get_collection_class(typename)
    check_permissions(ctx, user_id, permissions, CrudPermissions.CREATE, cls)
    try:
        instances = ctx.create_object(typename, json, user_id)
    except Exception as e:
        raise HTTPBadRequest(e)
    if instances:
        first = instances[0]
        db = first.db
        for instance in instances:
            db.add(instance)
        db.flush()
        view = request.GET.get('view', None) or 'default'
        return Response(
            dumps(first.generic_json(view, user_id, permissions)),
            location=first.uri_generic(first.id),
            status_code=201)
Example #25
0
def raise_if_cannot_moderate(request):
    ctx = request.context
    user_id = request.authenticated_userid
    if not user_id:
        raise HTTPUnauthorized()
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    if P_MODERATE not in permissions:
        raise HTTPUnauthorized()
Example #26
0
def get_idea_sibling_criteria(request):
    ctx = request.context
    view = (request.matchdict or {}).get('view', None)\
        or ctx.get_default_view() or 'default'
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    return [cr.generic_json(view, user_id, permissions) for cr in
            ctx._instance.get_siblings_of_type(Idea)]
Example #27
0
def instance_del(request):
    ctx = request.context
    user_id = authenticated_userid(request)
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    instance = ctx._instance
    if not instance.user_can(user_id, CrudPermissions.DELETE, permissions):
        return HTTPUnauthorized()
    instance.db.delete(instance)
    return {}
Example #28
0
def instance_view(request):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(
        user_id, ctx.get_discussion_id())
    instance = ctx._instance
    if not instance.user_can(user_id, CrudPermissions.READ, permissions):
        return HTTPUnauthorized()
    view = ctx.get_default_view() or 'default'
    view = request.GET.get('view', view)
    return instance.generic_json(view, user_id, permissions)
Example #29
0
def vote_results(request):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    widget = ctx.get_instance_of_class(MultiCriterionVotingWidget)
    if not widget:
        raise HTTPNotFound()
    if widget.activity_state != "ended":
        permissions = get_permissions(user_id, ctx.get_discussion_id())
        if P_ADMIN_DISC not in permissions:
            raise HTTPUnauthorized()
    return ctx._instance.all_voting_results()
Example #30
0
def get_post(request):
    post_id = request.matchdict['id']
    post = Post.get_instance(post_id)
    view_def = request.GET.get('view') or 'default'

    if not post:
        raise HTTPNotFound("Post with id '%s' not found." % post_id)
    discussion_id = int(request.matchdict['discussion_id'])
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(user_id, discussion_id)

    return post.generic_json(view_def, user_id, permissions)
Example #31
0
def upload_file(request):
    """
    POSTing a file upload is very different than any other endpoint in assembl
    API because all of the content will be passed in using a MULTIPART_HEADER,
    with all of data as well as the file (along with its metadata)
    """

    # Testing purposes on front-end
    # raise Exception("Upload file exception occured!")

    db = Document.default_db
    ctx = request.context
    user_id = request.authenticated_userid or Everyone
    discusison_id = ctx.get_discussion_id()
    discussion = Discussion.get(discusison_id)
    permissions = get_permissions(user_id, discusison_id)

    mime = request.POST['mime_type']
    file_name = request.POST['name']

    # Check if the file has previously existed, if so, change the name by appending "(n)"
    # to it's name

    try:
        blob = File(discussion=discussion,
                    mime_type=mime,
                    title=file_name)
        db.add(blob)
        with request.POST['file'].file as f:
            blob.add_file_data(f)
        db.flush()
    except Exception as e:
        raise HTTPServerError(e)

    view = 'default'
    return blob.generic_json(view, user_id, permissions)
Example #32
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        MEDIA_ATTACHMENT = models.AttachmentPurpose.MEDIA_ATTACHMENT.value
        cls = models.Idea
        phase_identifier = args.get('identifier')
        if phase_identifier == Phases.survey.value:
            cls = models.Thematic

        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(user_id, CrudPermissions.CREATE,
                                   permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush as db:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            title_langstring = langstring_from_input_entries(title_entries)
            description_langstring = langstring_from_input_entries(
                args.get('description_entries'))
            kwargs = {}
            if description_langstring is not None:
                kwargs['description'] = description_langstring

            kwargs['message_view_override'] = args.get('message_view_override')

            video = args.get('video')
            video_media = None
            if video is not None:
                video_title = langstring_from_input_entries(
                    video.get('title_entries', None))
                if video_title is not None:
                    kwargs['video_title'] = video_title

                video_description_top = langstring_from_input_entries(
                    video.get('description_entries_top', None))
                if video_description_top is not None:
                    kwargs['video_description_top'] = video_description_top

                video_description_bottom = langstring_from_input_entries(
                    video.get('description_entries_bottom', None))
                if video_description_bottom is not None:
                    kwargs[
                        'video_description_bottom'] = video_description_bottom

                video_description_side = langstring_from_input_entries(
                    video.get('description_entries_side', None))
                if video_description_side is not None:
                    kwargs['video_description_side'] = video_description_side

                video_html_code = video.get('html_code', None)
                if video_html_code is not None:
                    kwargs['video_html_code'] = video_html_code

                video_media = video.get('media_file', None)

            parent_idea_id = args.get('parent_id')
            if parent_idea_id:
                parent_idea_id = int(Node.from_global_id(parent_idea_id)[1])
                parent_idea = models.Idea.get(parent_idea_id)
                if not parent_idea:
                    raise Exception('Parent Idea not found')
                if parent_idea.discussion != discussion:
                    # No cross-debate references are allowed,
                    # for security reasons
                    raise Exception(
                        'Parent Idea does not belong to this discussion'
                    )  # noqa: E501
            else:
                if phase_identifier in (Phases.thread.value,
                                        Phases.multiColumns.value):
                    parent_idea = discussion.root_idea
                else:
                    # Our thematic, because it inherits from Idea, needs to be
                    # associated to the root idea of the discussion.
                    # We create a hidden root thematic, corresponding to the
                    # `identifier` phase, child of the root idea,
                    # and add our thematic as a child of this root thematic.
                    parent_idea = get_root_thematic_for_phase(
                        discussion, phase_identifier)
                    if parent_idea is None:
                        parent_idea = create_root_thematic(
                            discussion, phase_identifier)

            saobj = cls(discussion_id=discussion_id,
                        title=title_langstring,
                        **kwargs)
            if cls == models.Thematic:
                saobj.identifier = phase_identifier  # I don't think this is really used

            db.add(saobj)
            order = len(parent_idea.get_children()) + 1.0
            db.add(
                models.IdeaLink(source=parent_idea,
                                target=saobj,
                                order=args.get('order', order)))

            # Create the idea announcement object which corresponds to the instructions
            announcement = args.get('announcement')
            if announcement is not None:
                announcement_title_entries = announcement.get('title_entries')
                if len(announcement_title_entries) == 0:
                    raise Exception(
                        'Announcement titleEntries needs at least one entry')

                announcement_title_langstring = langstring_from_input_entries(
                    announcement_title_entries)
                announcement_body_langstring = langstring_from_input_entries(
                    announcement.get('body_entries', None))
                saobj2 = create_idea_announcement(
                    user_id, discussion, saobj, announcement_title_langstring,
                    announcement_body_langstring)
                db.add(saobj2)

            # add uploaded image as an attachment to the idea
            image = args.get('image')
            if image is not None:
                new_attachment = create_attachment(discussion,
                                                   models.IdeaAttachment,
                                                   image, EMBED_ATTACHMENT,
                                                   context)
                new_attachment.idea = saobj
                db.add(new_attachment)

            # add uploaded image as an attachment to the idea
            if video_media is not None:
                new_attachment = create_attachment(discussion,
                                                   models.IdeaAttachment,
                                                   video_media,
                                                   MEDIA_ATTACHMENT, context)
                new_attachment.idea = saobj
                db.add(new_attachment)

            questions_input = args.get('questions')
            if questions_input is not None:
                for idx, question_input in enumerate(questions_input):
                    title_ls = langstring_from_input_entries(
                        question_input['title_entries'])
                    question = models.Question(title=title_ls,
                                               discussion_id=discussion_id)
                    db.add(
                        models.IdeaLink(source=saobj,
                                        target=question,
                                        order=idx + 1.0))

        db.flush()
        return CreateThematic(thematic=saobj)
Example #33
0
def _get_ideas_real(discussion, view_def=None, ids=None, user_id=None):
    user_id = user_id or Everyone
    # optimization: Recursive widget links.
    from assembl.models import (Widget, IdeaWidgetLink,
                                IdeaDescendantsShowingWidgetLink)
    universal_widget_links = []
    by_idea_widget_links = defaultdict(list)
    widget_links = discussion.db.query(IdeaWidgetLink).join(Widget).join(
        Discussion).filter(
            Widget.test_active(), Discussion.id == discussion.id,
            IdeaDescendantsShowingWidgetLink.polymorphic_filter()).options(
                joinedload_all(IdeaWidgetLink.idea)).all()
    for wlink in widget_links:
        if isinstance(wlink.idea, RootIdea):
            universal_widget_links.append({
                '@type':
                wlink.external_typename(),
                'widget':
                Widget.uri_generic(wlink.widget_id)
            })
        else:
            for id in wlink.idea.get_all_descendants(True):
                by_idea_widget_links[Idea.uri_generic(id)].append({
                    '@type':
                    wlink.external_typename(),
                    'widget':
                    Widget.uri_generic(wlink.widget_id)
                })

    next_synthesis = discussion.get_next_synthesis()
    ideas = discussion.db.query(Idea).filter_by(discussion_id=discussion.id)

    ideas = ideas.outerjoin(
        SubGraphIdeaAssociation,
        and_(SubGraphIdeaAssociation.sub_graph_id == next_synthesis.id,
             SubGraphIdeaAssociation.idea_id == Idea.id))

    ideas = ideas.outerjoin(IdeaLink, and_(IdeaLink.target_id == Idea.id))

    ideas = ideas.order_by(IdeaLink.order, Idea.creation_date)

    if ids:
        ids = [get_database_id("Idea", id) for id in ids]
        ideas = ideas.filter(Idea.id.in_(ids))
    # remove tombstones
    ideas = ideas.filter(and_(*Idea.base_conditions()))
    ideas = ideas.options(joinedload_all(Idea.source_links),
                          joinedload_all(Idea.attachments),
                          joinedload_all(Idea.message_columns),
                          joinedload_all(Idea.has_showing_widget_links),
                          undefer(Idea.num_children))

    permissions = get_permissions(user_id, discussion.id)
    Idea.prepare_counters(discussion.id, True)
    retval = [
        idea.generic_json(view_def, user_id, permissions) for idea in ideas
    ]
    retval = [x for x in retval if x is not None]
    for r in retval:
        if r.get('widget_links', None) is not None:
            links = r['widget_links'][:]
            links.extend(universal_widget_links)
            links.extend(by_idea_widget_links[r['@id']])
            r['active_widget_links'] = links
    return retval
Example #34
0
    def mutate(root, args, context, info):
        cls = models.Discussion
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = discussion.user_can(
            user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush as db:
            legal_notice_entries = args.get('legal_notice_entries')
            if legal_notice_entries is not None and len(legal_notice_entries) == 0:
                raise Exception(
                    'Legal notice entries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(
                discussion, 'legal_notice', legal_notice_entries)

            terms_and_conditions_entries = args.get(
                'terms_and_conditions_entries')
            if terms_and_conditions_entries is not None and len(terms_and_conditions_entries) == 0:
                raise Exception(
                    'Terms and conditions entries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(
                discussion, 'terms_and_conditions', terms_and_conditions_entries)

            cookies_policy_entries = args.get('cookies_policy_entries')
            if cookies_policy_entries is not None and len(cookies_policy_entries) == 0:
                raise Exception(
                    'Cookies policy entries needs at least one entry')

            update_langstring_from_input_entries(
                discussion, 'cookies_policy', cookies_policy_entries)

            privacy_policy_entries = args.get('privacy_policy_entries')
            if privacy_policy_entries is not None and len(privacy_policy_entries) == 0:
                raise Exception(
                    'Privacy policy entries need to be at least one entry'
                )

            update_langstring_from_input_entries(
                discussion, 'privacy_policy', privacy_policy_entries)

            user_guidelines_entries = args.get('user_guidelines_entries')
            if user_guidelines_entries is not None and len(user_guidelines_entries) == 0:
                raise Exception(
                    'User guidelines entries need to be at least one entry'
                )

            update_langstring_from_input_entries(
                discussion, 'user_guidelines', user_guidelines_entries
            )

        db.flush()
        legal_contents = LegalContents()
        return UpdateLegalContents(legal_contents=legal_contents)
Example #35
0
def get_posts(request):
    """
    Query interface on posts
    Filters have two forms:
    only_*, is for filters that cannot be reversed (ex: only_synthesis, only_orphan)
    is_*, is for filters that can be reversed (ex:is_unread=true returns only unread
     message, is_unread=false returns only read messages)
    order: can be chronological, reverse_chronological, popularity
    root_post_id: all posts below the one specified.
    family_post_id: all posts below the one specified, and all its ancestors.
    post_reply_to: replies to a given post
    root_idea_id: all posts associated with the given idea
    ids: explicit message ids.
    posted_after_date, posted_before_date: date selection (ISO format)
    post_author: filter by author
    classifier: filter on message_classifier, or absence thereof (classifier=null). Can be negated with "!"
    """
    localizer = request.localizer
    discussion_id = int(request.matchdict['discussion_id'])
    discussion = Discussion.get(int(discussion_id))
    if not discussion:
        raise HTTPNotFound(
            localizer.translate(_("No discussion found with id=%s")) %
            discussion_id)

    discussion.import_from_sources()

    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(user_id, discussion_id)

    DEFAULT_PAGE_SIZE = 25
    page_size = DEFAULT_PAGE_SIZE

    filter_names = [
        filter_name for filter_name in request.GET.getone('filters').split(',')
        if filter_name
    ] if request.GET.get('filters') else []

    try:
        page = int(request.GET.getone('page'))
    except (ValueError, KeyError):
        page = 1

    text_search = request.GET.get('text_search', None)

    order = request.GET.get('order')
    if order is None:
        order = 'chronological'
    assert order in ('chronological', 'reverse_chronological', 'score',
                     'popularity')
    if order == 'score':
        assert text_search is not None

    if page < 1:
        page = 1

    root_post_id = request.GET.getall('root_post_id')
    if root_post_id:
        root_post_id = get_database_id("Post", root_post_id[0])
    family_post_id = request.GET.getall('family_post_id')
    if family_post_id:
        family_post_id = get_database_id("Post", family_post_id[0])

    root_idea_id = request.GET.getall('root_idea_id')
    if root_idea_id:
        root_idea_id = get_database_id("Idea", root_idea_id[0])

    ids = request.GET.getall('ids[]')
    if ids:
        ids = [get_database_id("Post", id) for id in ids]

    view_def = request.GET.get('view') or 'default'

    only_synthesis = request.GET.get('only_synthesis')

    post_author_id = request.GET.get('post_author')
    if post_author_id:
        post_author_id = get_database_id("AgentProfile", post_author_id)
        assert AgentProfile.get(
            post_author_id
        ), "Unable to find agent profile with id " + post_author_id

    post_replies_to = request.GET.get('post_replies_to')
    if post_replies_to:
        post_replies_to = get_database_id("AgentProfile", post_replies_to)
        assert AgentProfile.get(
            post_replies_to
        ), "Unable to find agent profile with id " + post_replies_to

    posted_after_date = request.GET.get('posted_after_date')
    posted_before_date = request.GET.get('posted_before_date')
    message_classifiers = request.GET.getall('classifier')

    PostClass = SynthesisPost if only_synthesis == "true" else Post
    if order == 'score':
        posts = discussion.db.query(PostClass,
                                    Content.body_text_index.score_name)
    else:
        posts = discussion.db.query(PostClass)

    posts = posts.filter(PostClass.discussion_id == discussion_id, )
    ##no_of_posts_to_discussion = posts.count()

    post_data = []

    # True means deleted only, False (default) means non-deleted only. None means both.

    # v0
    # deleted = request.GET.get('deleted', None)
    # end v0

    # v1: we would like something like that
    # deleted = request.GET.get('deleted', None)
    # if deleted is None:
    #     if view_def == 'id_only':
    #         deleted = None
    #     else:
    #         deleted = False
    # end v1

    # v2
    # deleted = request.GET.get('deleted', None)
    # if deleted is None:
    #     if not ids:
    #         deleted = False
    #     else:
    #         deleted = None
    #
    # if deleted == 'false':
    #     deleted = False
    #     posts = posts.filter(PostClass.tombstone_condition())
    # elif deleted == 'true':
    #     deleted = True
    #     posts = posts.filter(PostClass.not_tombstone_condition())
    # elif deleted == 'any':
    #     deleted = None
    #     # result will contain deleted and non-deleted posts
    #     pass
    # end v2

    # v3
    # deleted = request.GET.get('deleted', None)
    # if deleted is None:
    #     if not ids:
    #         deleted = False
    #     else:
    #         deleted = None

    # if deleted == 'true':
    #     deleted = True
    #     posts = posts.filter(PostClass.not_tombstone_condition())
    # end v3

    # v4
    deleted = request.GET.get('deleted', None)
    if deleted is None:
        if not ids:
            deleted = False
        else:
            deleted = None
    elif deleted.lower() == "any":
        deleted = None
    else:
        deleted = asbool(deleted)
    # if deleted is not in (False, True, None):
    #    deleted = False
    # end v4

    only_orphan = asbool(request.GET.get('only_orphan', False))
    if only_orphan:
        if root_idea_id:
            raise HTTPBadRequest(
                localizer.translate(
                    _("Getting orphan posts of a specific idea isn't supported."
                      )))
        orphans = Idea._get_orphan_posts_statement(
            discussion_id, True, include_deleted=deleted).subquery("orphans")
        posts = posts.join(orphans, PostClass.id == orphans.c.post_id)

    if root_idea_id:
        related = Idea.get_related_posts_query_c(discussion_id,
                                                 root_idea_id,
                                                 True,
                                                 include_deleted=deleted)
        posts = posts.join(related, PostClass.id == related.c.post_id)
    elif not only_orphan:
        if deleted is not None:
            if deleted:
                posts = posts.filter(
                    PostClass.publication_state.in_(
                        deleted_publication_states))
            else:
                posts = posts.filter(PostClass.tombstone_date == None)

    if root_post_id:
        root_post = Post.get(root_post_id)

        posts = posts.filter((Post.ancestry.like(root_post.ancestry +
                                                 cast(root_post.id, String) +
                                                 ',%'))
                             | (PostClass.id == root_post.id))
    elif family_post_id:
        root_post = Post.get(family_post_id)
        ancestor_ids = root_post.ancestor_ids()
        posts = posts.filter((Post.ancestry.like(root_post.ancestry +
                                                 cast(root_post.id, String) +
                                                 ',%'))
                             | (PostClass.id == root_post.id)
                             | (PostClass.id.in_(ancestor_ids)))
    else:
        root_post = None

    if ids:
        posts = posts.filter(Post.id.in_(ids))

    if posted_after_date:
        posted_after_date = parse_datetime(posted_after_date)
        if posted_after_date:
            posts = posts.filter(PostClass.creation_date >= posted_after_date)
        #Maybe we should do something if the date is invalid.  benoitg

    if posted_before_date:
        posted_before_date = parse_datetime(posted_before_date)
        if posted_before_date:
            posts = posts.filter(PostClass.creation_date <= posted_before_date)
        #Maybe we should do something if the date is invalid.  benoitg

    if post_author_id:
        posts = posts.filter(PostClass.creator_id == post_author_id)

    if message_classifiers:
        if any([len(classifier) == 0 for classifier in message_classifiers]):
            return {'total': 0, 'posts': []}
        polarities = [
            classifier[0] != "!" for classifier in message_classifiers
        ]
        polarity = all(polarities)
        if not polarity:
            message_classifiers = [c.strip("!") for c in message_classifiers]
        if polarity != any(polarities):
            raise HTTPBadRequest(
                _("Do not combine negative and positive classifiers"))
        # Treat null as no classifier
        includes_null = 'null' in message_classifiers
        if includes_null:
            message_classifiers_nonull = filter(lambda c: c != "null",
                                                message_classifiers)
        if polarity:
            if len(message_classifiers) == 1:
                term = PostClass.message_classifier == (
                    None if includes_null else message_classifiers[0])
            else:
                term = PostClass.message_classifier.in_(
                    message_classifiers_nonull)
                if includes_null:
                    term = term | (PostClass.message_classifier == None)
        else:
            if len(message_classifiers) == 1:
                term = PostClass.message_classifier != (
                    None if includes_null else message_classifiers[0])
            else:
                term = PostClass.message_classifier.notin_(
                    message_classifiers_nonull)
            if not includes_null:
                term = term | (PostClass.message_classifier == None)
        posts = posts.filter(term)

    if post_replies_to:
        parent_alias = aliased(PostClass)
        posts = posts.join(parent_alias, PostClass.parent)
        posts = posts.filter(parent_alias.creator_id == post_replies_to)
    # Post read/unread management
    is_unread = request.GET.get('is_unread')
    translations = None
    if user_id != Everyone:
        # This is horrible, but the join creates complex subqueries that
        # virtuoso cannot decode properly.
        read_posts = {
            v.post_id
            for v in discussion.db.query(ViewPost).filter(
                ViewPost.tombstone_condition(), ViewPost.actor_id == user_id,
                *ViewPost.get_discussion_conditions(discussion_id))
        }
        my_sentiments = {
            l.post_id: l
            for l in discussion.db.query(SentimentOfPost).filter(
                SentimentOfPost.tombstone_condition(), SentimentOfPost.actor_id
                == user_id,
                *SentimentOfPost.get_discussion_conditions(discussion_id))
        }
        if is_unread != None:
            posts = posts.outerjoin(
                ViewPost,
                and_(ViewPost.actor_id == user_id,
                     ViewPost.post_id == PostClass.id,
                     ViewPost.tombstone_date == None))
            if is_unread == "true":
                posts = posts.filter(ViewPost.id == None)
            elif is_unread == "false":
                posts = posts.filter(ViewPost.id != None)
        user = AgentProfile.get(user_id)
        service = discussion.translation_service()
        if service:
            translations = PrefCollectionTranslationTable(
                service, LanguagePreferenceCollection.getCurrent(request))
    else:
        #If there is no user_id, all posts are always unread
        my_sentiments = {}
        if is_unread == "false":
            raise HTTPBadRequest(
                localizer.translate(
                    _("You must be logged in to view which posts are read")))

    if text_search is not None:
        # another Virtuoso bug: offband kills score. but it helps speed.
        offband = () if (order == 'score') else None
        posts = posts.filter(
            Post.body_text_index.contains(text_search.encode('utf-8'),
                                          offband=offband))

    # posts = posts.options(contains_eager(Post.source))
    # Horrible hack... But useful for structure load
    if view_def == 'id_only':
        pass  # posts = posts.options(defer(Post.body))
    else:
        ideaContentLinkQuery = posts.with_entities(
            PostClass.id, PostClass.idea_content_links_above_post)
        ideaContentLinkCache = dict(ideaContentLinkQuery.all())
        # Note: we could count the like the same way and kill the subquery.
        # But it interferes with the popularity order,
        # and the benefit is not that high.
        sentiment_counts = discussion.db.query(
            PostClass.id, SentimentOfPost.type,
            count(SentimentOfPost.id)).join(SentimentOfPost).filter(
                PostClass.id.in_(posts.with_entities(PostClass.id).subquery()),
                SentimentOfPost.tombstone_condition()).group_by(
                    PostClass.id, SentimentOfPost.type)
        sentiment_counts_by_post_id = defaultdict(dict)
        for (post_id, sentiment_type, sentiment_count) in sentiment_counts:
            sentiment_counts_by_post_id[post_id][sentiment_type[
                SentimentOfPost.TYPE_PREFIX_LEN:]] = sentiment_count
        posts = posts.options(
            # undefer(Post.idea_content_links_above_post),
            joinedload_all(Post.creator),
            joinedload_all(Post.extracts),
            joinedload_all(Post.widget_idea_links),
            joinedload_all(SynthesisPost.publishes_synthesis),
            subqueryload_all(Post.attachments))
        if len(discussion.discussion_locales) > 1:
            posts = posts.options(*Content.subqueryload_options())
        else:
            posts = posts.options(*Content.joinedload_options())

    if order == 'chronological':
        posts = posts.order_by(Content.creation_date)
    elif order == 'reverse_chronological':
        posts = posts.order_by(Content.creation_date.desc())
    elif order == 'score':
        posts = posts.order_by(Content.body_text_index.score_name.desc())
    elif order == 'popularity':
        # assume reverse chronological otherwise
        posts = posts.order_by(Content.disagree_count - Content.like_count,
                               Content.creation_date.desc())
    else:
        posts = posts.order_by(Content.id)
    # print str(posts)

    no_of_posts = 0
    no_of_posts_viewed_by_user = 0

    if deleted is True:
        # We just got deleted posts, now we want their ancestors for context
        post_ids = set()
        ancestor_ids = set()

        def add_ancestors(post):
            post_ids.add(post.id)
            ancestor_ids.update(
                [int(x) for x in post.ancestry.strip(",").split(",") if x])

        posts = list(posts)
        for post in posts:
            add_ancestors(post)
        ancestor_ids -= post_ids
        if ancestor_ids:
            ancestors = discussion.db.query(PostClass).filter(
                PostClass.id.in_(ancestor_ids))
            if view_def == 'id_only':
                pass  # ancestors = ancestors.options(defer(Post.body))
            else:
                ancestors = ancestors.options(
                    # undefer(Post.idea_content_links_above_post),
                    joinedload_all(Post.creator),
                    joinedload_all(Post.extracts),
                    joinedload_all(Post.widget_idea_links),
                    joinedload_all(SynthesisPost.publishes_synthesis),
                    subqueryload_all(Post.attachments))
                if len(discussion.discussion_locales) > 1:
                    ancestors = ancestors.options(
                        *Content.subqueryload_options())
                else:
                    ancestors = ancestors.options(
                        *Content.joinedload_options())
            posts.extend(ancestors.all())

    for query_result in posts:
        score, viewpost = None, None
        if not isinstance(query_result, (list, tuple)):
            query_result = [query_result]
        post = query_result[0]
        if deleted is True:
            add_ancestors(post)

        if user_id != Everyone:
            viewpost = post.id in read_posts
            if view_def != "id_only":
                translate_content(post,
                                  translation_table=translations,
                                  service=service)
        no_of_posts += 1
        serializable_post = post.generic_json(view_def, user_id,
                                              permissions) or {}
        if order == 'score':
            score = query_result[1]
            serializable_post['score'] = score

        if viewpost:
            serializable_post['read'] = True
            no_of_posts_viewed_by_user += 1
        elif user_id != Everyone and root_post is not None and root_post.id == post.id:
            # Mark post read, we requested it explicitely
            viewed_post = ViewPost(actor_id=user_id, post=root_post)
            discussion.db.add(viewed_post)
            serializable_post['read'] = True
        else:
            serializable_post['read'] = False
        my_sentiment = my_sentiments.get(post.id, None)
        if my_sentiment is not None:
            my_sentiment = my_sentiment.generic_json('default', user_id,
                                                     permissions)
        serializable_post['my_sentiment'] = my_sentiment
        if view_def != "id_only":
            serializable_post['indirect_idea_content_links'] = (
                post.indirect_idea_content_links_with_cache(
                    ideaContentLinkCache.get(post.id, None)))
            serializable_post[
                'sentiment_counts'] = sentiment_counts_by_post_id[post.id]

        post_data.append(serializable_post)

    # Benoitg:  For now, this completely garbles threading without intelligent
    #handling of pagination.  Disabling
    #posts = posts.limit(page_size).offset(data['startIndex']-1)
    # This code isn't up to date.  If limiting the query by page, we need to
    # calculate the counts with a separate query to have the right number of
    # results
    #no_of_messages_viewed_by_user = discussion.db.query(ViewPost).join(
    #    Post
    #).filter(
    #    Post.discussion_id == discussion_id,
    #    ViewPost.actor_id == user_id,
    #).count() if user_id else 0

    data = {}
    data["page"] = page
    data["unread"] = no_of_posts - no_of_posts_viewed_by_user
    data["total"] = no_of_posts
    data["maxPage"] = max(1, ceil(float(data["total"]) / page_size))
    #TODO:  Check if we want 1 based index in the api
    data["startIndex"] = (page_size * page) - (page_size - 1)

    if data["page"] == data["maxPage"]:
        data["endIndex"] = data["total"]
    else:
        data["endIndex"] = data["startIndex"] + (page_size - 1)
    data["posts"] = post_data

    return data
Example #36
0
def global_vote_results_csv(request):
    ctx = request.context
    user_id = request.authenticated_userid
    if not user_id:
        raise HTTPUnauthorized
    widget = ctx._instance
    if widget.activity_state != "ended":
        permissions = get_permissions(user_id, ctx.get_discussion_id())
        if P_ADMIN_DISC not in permissions:
            raise HTTPUnauthorized()
    user_prefs = LanguagePreferenceCollection.getCurrent()
    # first fetch the ideas voted on
    ideas = widget.db.query(Idea).join(AbstractVoteSpecification).filter(
        AbstractVoteSpecification.widget_id == widget.id).distinct().all()
    rowtitles = [(idea.safe_title(user_prefs, request.localizer), idea.id)
                 for idea in ideas]
    rowtitles.sort()
    specs = widget.vote_specifications
    # specs and their templates
    specids_by_template_specid = defaultdict(list)
    spec_by_idea_id_and_template_specid = {}
    for spec in specs:
        specids_by_template_specid[spec.vote_spec_template_id
                                   or spec.id].append(spec.id)
        spec_by_idea_id_and_template_specid[(spec.criterion_idea_id,
                                             (spec.vote_spec_template_id
                                              or spec.id))] = spec
    # then get the vote specs templates only
    template_specs = [(spec.title.best_lang(user_prefs).value
                       if spec.title else str(spec.id), spec)
                      for spec in widget.specification_templates]
    template_specs.sort()
    coltitles = ["Proposition", "Nombre de participants sur la proposition"]

    # number of participants for a proposal (distinct voter_id from all specs related to the proposal)
    num_participants_by_idea_id = {}
    for idea in ideas:
        vote_specifications = idea.criterion_for
        query = vote_specifications[0].get_voter_ids_query()
        for vote_spec in vote_specifications[1:]:
            query = query.union(vote_spec.get_voter_ids_query())
        num_participants_by_idea_id[idea.id] = query.count()

    # construct a query with each votespec creating columns for:
    # either each token count (for token votes) OR
    # sum of vote values, and count of votes otherwise.
    # Ideas are rows (and Idea.id is column 0)
    for title, template_spec in template_specs:
        if isinstance(template_spec, TokenVoteSpecification):
            for tokencat in template_spec.token_categories:
                coltitles.append(
                    tokencat.name.best_lang(user_prefs).value.encode('utf-8'))
        else:
            coltitles.append(
                u'{title} - moyenne'.format(title=title).encode('utf-8'))
            if isinstance(template_spec, NumberGaugeVoteSpecification):
                for choice_value in range_float(template_spec.minimum,
                                                template_spec.maximum,
                                                template_spec.nb_ticks):
                    coltitles.append(u'{value} {unit}'.format(
                        value=choice_value,
                        unit=template_spec.unit).encode('utf-8'))
            else:
                for choice in template_spec.get_choices():
                    coltitles.append(
                        choice.label.best_lang(user_prefs).value.encode(
                            'utf-8'))
        coltitles.append('Total votes')

    output = StringIO()
    # include BOM for Excel to open the file in UTF-8 properly
    output.write(u'\ufeff'.encode('utf-8'))
    csvw = csv.writer(output)
    csvw.writerow(coltitles)
    from assembl.graphql.vote_session import get_avg_choice
    for title, idea_id in rowtitles:
        row = [title.encode('utf-8'), num_participants_by_idea_id[idea_id]]
        for t, template_spec in template_specs:
            spec = spec_by_idea_id_and_template_specid.get(
                (idea_id, template_spec.id), None)
            if isinstance(template_spec, TokenVoteSpecification):
                for token_category in template_spec.token_categories:
                    if spec is None:
                        row.append('-')
                    else:
                        query = spec.db.query(
                            func.sum(
                                getattr(
                                    spec.get_vote_class(),
                                    "vote_value"))).filter_by(
                                        vote_spec_id=spec.id,
                                        tombstone_date=None,
                                        token_category_id=token_category.id)
                        # when there is no votes, query.first() equals (None,)
                        # in this case set num_token to 0
                        num_token = query.first()[0]
                        row.append(num_token or "-")
            else:  # this is a number or text gauge
                if spec is None:
                    row.append('-')
                    if isinstance(template_spec, NumberGaugeVoteSpecification):
                        for choice_value in range_float(
                                template_spec.minimum, template_spec.maximum,
                                template_spec.nb_ticks):
                            row.append('-')
                    else:
                        for choice in template_spec.get_choices():
                            row.append('-')
                elif isinstance(template_spec, NumberGaugeVoteSpecification):
                    vote_cls = spec.get_vote_class()
                    voting_avg = spec.db.query(
                        func.avg(getattr(vote_cls, 'vote_value'))).filter_by(
                            vote_spec_id=spec.id,
                            tombstone_date=None,
                            idea_id=spec.criterion_idea_id).first()
                    # when there is no votes, query.first() equals (None,)
                    avg = voting_avg[0] or '-'
                    row.append(avg)

                    q_histogram = spec.db.query(
                        getattr(vote_cls, 'vote_value'),
                        func.count(getattr(vote_cls, 'voter_id'))).filter_by(
                            vote_spec_id=spec.id,
                            tombstone_date=None,
                            idea_id=spec.criterion_idea_id).group_by(
                                getattr(vote_cls, 'vote_value'))
                    histogram = dict(q_histogram.all())
                    for choice_value in range_float(template_spec.minimum,
                                                    template_spec.maximum,
                                                    template_spec.nb_ticks):
                        row.append(histogram.get(choice_value, 0))
                else:
                    vote_cls = spec.get_vote_class()
                    avg_choice = get_avg_choice(spec)
                    if not avg_choice:
                        label_avg = '-'
                    else:
                        label_avg = avg_choice.label.best_lang(
                            user_prefs).value.encode('utf-8')
                    row.append(label_avg)

                    q_histogram = spec.db.query(
                        getattr(vote_cls, 'vote_value'),
                        func.count(getattr(vote_cls, 'voter_id'))).filter_by(
                            vote_spec_id=spec.id,
                            tombstone_date=None,
                            idea_id=spec.criterion_idea_id).group_by(
                                getattr(vote_cls, 'vote_value'))
                    histogram = dict(q_histogram.all())
                    for choice in template_spec.get_choices():
                        row.append(histogram.get(choice.value, 0))

            if spec is None:
                row.append('-')
            else:
                num_votes = spec.db.query(
                    getattr(spec.get_vote_class(), "voter_id")).filter_by(
                        vote_spec_id=spec.id, tombstone_date=None).count()
                row.append(num_votes)
        csvw.writerow(row)
    output.seek(0)
    return Response(
        body_file=output,
        content_type='text/csv',
        content_disposition='attachment; filename="vote_results.csv"')
Example #37
0
def assembl_register_user(request):
    forget(request)
    localizer = request.localizer
    session = AgentProfile.default_db
    json = request.json
    logger = logging.getLogger()
    discussion = discussion_from_request(request)
    permissions = get_permissions(Everyone,
                                  discussion.id if discussion else None)

    name = json.get('real_name', '').strip()
    errors = JSONError()
    if not name or len(name) < 3:
        errors.add_error(
            localizer.translate(
                _("Please use a name of at least 3 characters")),
            ErrorTypes.SHORT_NAME)
    password = json.get('password', '').strip()
    # TODO: Check password strength. maybe pwdmeter?
    email = None
    for account in json.get('accounts', ()):
        email = account.get('email', None)
        if not is_email(email):
            errors.add_error(
                localizer.translate(_("This is not a valid email")),
                ErrorTypes.INVALID_EMAIL)
            continue
        email = EmailString.normalize_email_case(email)
        # Find agent account to avoid duplicates!
        if session.query(AbstractAgentAccount).filter_by(
                email_ci=email).count():
            if not discussion.preferences['generic_errors']:
                errors.add_error(
                    localizer.translate(
                        _("We already have a user with this email.")),
                    ErrorTypes.EXISTING_EMAIL, HTTPConflict.code)
            else:
                errors.add_error(localizer.translate(generic_error_message),
                                 ErrorTypes.GENERIC, HTTPConflict.code)
                logger.error(
                    "[User creation]: We already have a user with this email %s"
                    % email)

    if not email:
        errors.add_error(localizer.translate(_("No email.")),
                         ErrorTypes.INVALID_EMAIL)
    username = json.get('username', None)
    if username:
        if session.query(Username).filter(
                func.lower(Username.username) == username.lower()).count():
            if not discussion.preferences['generic_errors']:
                errors.add_error(
                    localizer.translate(
                        _("We already have a user with this username.")),
                    ErrorTypes.EXISTING_USERNAME, HTTPConflict.code)
            else:
                errors.add_error(localizer.translate(generic_error_message),
                                 ErrorTypes.GENERIC, HTTPConflict.code)
                logger.error("We already have a user with username %s" %
                             username)
        if len(username) > 20:
            errors.add_error(
                localizer.translate(
                    _("The username must be less than 20 characters.")),
                ErrorTypes.USERNAME_TOO_LONG, HTTPBadRequest.code)
    if discussion:
        check_subscription = discussion.preferences['whitelist_on_register']
        whitelist = discussion.preferences['require_email_domain']
        if check_subscription and whitelist:
            status = discussion.check_email(email)
            if not status:
                admin_emails = discussion.get_admin_emails()
                num = len(admin_emails)
                errors.add_error(
                    localizer.pluralize(
                        _("Your email domain has not been approved for registration. Please contact ${emails} for support."
                          ),
                        _("Your email domain has not been approved for registration. Please contact one of ${emails} for support."
                          ),
                        num,
                        mapping={'emails': ", ".join(admin_emails)}))
    if errors:
        raise errors

    # This logic needs to be above the JSONError checks to ensure that whitelisting is applied
    # even if the discussion does not have a P_SELF_REGISTER on system.Everyone
    if discussion and not (P_SELF_REGISTER in permissions
                           or P_SELF_REGISTER_REQUEST in permissions):
        # Consider it without context
        discussion = None

    validate_registration = asbool(
        config.get('assembl.validate_registration_emails'))

    old_autoflush = session.autoflush
    session.autoflush = False
    try:
        now = datetime.utcnow()
        user = User(name=name,
                    password=password,
                    verified=not validate_registration,
                    creation_date=now)

        session.add(user)
        session.flush()

        user.update_from_json(json, user_id=user.id)
        account = user.accounts[0]
        email = account.email
        account.verified = not validate_registration
        if discussion:
            agent_status = AgentStatusInDiscussion(
                agent_profile=user,
                discussion=discussion,
                first_visit=now,
                last_visit=now,
                user_created_on_this_discussion=True)
            session.add(agent_status)
        session.flush()

        # create the profile fields for custom fields
        for global_id, value in json.get('profileFields', {}).iteritems():
            configurable_field_id = from_global_id(global_id)[1]
            configurable_field = AbstractConfigurableField.get(
                configurable_field_id)
            profile_field = ProfileField(
                agent_profile=user,
                configurable_field=configurable_field,
                discussion=configurable_field.discussion,
                value_data={u'value': value})
            session.add(profile_field)

        session.flush()

        if validate_registration:
            send_confirmation_email(request, account)
        else:
            user.verified = True
            for account in user.accounts:
                account.verified = True
            user.successful_login()
            if asbool(config.get('pyramid.debug_authorization')):
                # for debugging purposes
                from assembl.auth.password import email_token
                print "email token:", request.route_url(
                    'user_confirm_email', token=email_token(account))
            if discussion:
                check_subscription = discussion.preferences[
                    'whitelist_on_register']
                maybe_auto_subscribe(user,
                                     discussion,
                                     check_authorization=check_subscription)
        session.flush()
        return CreationResponse(user, Everyone, permissions)
    finally:
        session.autoflush = old_autoflush
Example #38
0
    def mutate(root, args, context, info):
        ATTACHMENT_PURPOSE_IMAGE = models.AttachmentPurpose.IMAGE.value
        ATTACHMENT_PURPOSE_DOCUMENT = models.AttachmentPurpose.DOCUMENT.value
        cls = models.Resource
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        resource_id = args.get('id')
        resource_id = int(Node.from_global_id(resource_id)[1])
        resource = cls.get(resource_id)

        permissions = get_permissions(user_id, discussion_id)
        allowed = resource.user_can(
            user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if title_entries is not None and len(title_entries) == 0:
                raise Exception(
                    'Resource titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(
                resource, 'title', title_entries)
            update_langstring_from_input_entries(
                resource, 'text', args.get('text_entries'))
            kwargs = {}
            kwargs['embed_code'] = args.get('embed_code', None)
            for attr, value in kwargs.items():
                setattr(resource, attr, value)

            db = resource.db

            # add uploaded image as an attachment to the resource
            image = args.get('image')
            if image is not None:
                filename = os.path.basename(context.POST[image].filename)
                mime_type = context.POST[image].type
                document = models.File(
                    discussion=discussion,
                    mime_type=mime_type,
                    title=filename)
                document.add_file_data(context.POST[image].file)
                # if there is already an IMAGE, remove it with the
                # associated document
                images = [
                    att for att in resource.attachments
                    if att.attachmentPurpose == ATTACHMENT_PURPOSE_IMAGE]
                if images:
                    image = images[0]
                    image.document.delete_file()
                    db.delete(image.document)
                    resource.attachments.remove(image)

                db.add(models.ResourceAttachment(
                    document=document,
                    discussion=discussion,
                    resource=resource,
                    creator_id=context.authenticated_userid,
                    title=filename,
                    attachmentPurpose=ATTACHMENT_PURPOSE_IMAGE
                ))

        # add uploaded doc as an attachment to the resource
        doc = args.get('doc')
        if doc is not None:
            filename = os.path.basename(context.POST[doc].filename)
            mime_type = context.POST[doc].type
            document = models.File(
                discussion=discussion,
                mime_type=mime_type,
                title=filename)
            document.add_file_data(context.POST[doc].file)
            # if there is already a DOCUMENT, remove it with the
            # associated document
            docs = [
                att for att in resource.attachments
                if att.attachmentPurpose == ATTACHMENT_PURPOSE_DOCUMENT]
            if docs:
                doc = docs[0]
                doc.document.delete_file()
                db.delete(doc.document)
                resource.attachments.remove(doc)

            resource.db.add(models.ResourceAttachment(
                document=document,
                discussion=discussion,
                resource=resource,
                creator_id=context.authenticated_userid,
                title=filename,
                attachmentPurpose=ATTACHMENT_PURPOSE_DOCUMENT
            ))

            db.flush()

        return UpdateResource(resource=resource)
Example #39
0
    def mutate(root, args, context, info):
        ATTACHMENT_PURPOSE_IMAGE = models.AttachmentPurpose.IMAGE.value
        ATTACHMENT_PURPOSE_DOCUMENT = models.AttachmentPurpose.DOCUMENT.value
        cls = models.Resource
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(
            user_id, CrudPermissions.CREATE, permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception(
                    'Resource titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            title_langstring = langstring_from_input_entries(title_entries)
            text_langstring = langstring_from_input_entries(
                args.get('text_entries'))
            kwargs = {}
            if text_langstring is not None:
                kwargs['text'] = text_langstring

            kwargs['embed_code'] = args.get('embed_code')
            saobj = cls(
                discussion_id=discussion_id,
                title=title_langstring,
                **kwargs)
            db = saobj.db
            db.add(saobj)

            image = args.get('image')
            if image is not None:
                filename = os.path.basename(context.POST[image].filename)
                mime_type = context.POST[image].type
                document = models.File(
                    discussion=discussion,
                    mime_type=mime_type,
                    title=filename)
                document.add_file_data(context.POST[image].file)
                db.add(models.ResourceAttachment(
                    document=document,
                    resource=saobj,
                    discussion=discussion,
                    creator_id=context.authenticated_userid,
                    title=filename,
                    attachmentPurpose=ATTACHMENT_PURPOSE_IMAGE
                ))

            doc = args.get('doc')
            if doc is not None:
                filename = os.path.basename(context.POST[doc].filename)
                mime_type = context.POST[doc].type
                document = models.File(
                    discussion=discussion,
                    mime_type=mime_type,
                    title=filename)
                document.add_file_data(context.POST[doc].file)
                db.add(models.ResourceAttachment(
                    document=document,
                    resource=saobj,
                    discussion=discussion,
                    creator_id=context.authenticated_userid,
                    title=filename,
                    attachmentPurpose=ATTACHMENT_PURPOSE_DOCUMENT
                ))

            db.flush()

        return CreateResource(resource=saobj)
Example #40
0
def get_posts(request):
    """
    Query interface on posts
    Filters have two forms:
    only_*, is for filters that cannot be reversed (ex: only_synthesis)
    is_*, is for filters that can be reversed (ex:is_unread=true returns only unread
    order can be chronological, reverse_chronological
    message, is_unread=false returns only read messages)
    """
    localizer = request.localizer
    discussion_id = int(request.matchdict['discussion_id'])
    discussion = Discussion.get(int(discussion_id))
    if not discussion:
        raise HTTPNotFound(
            localizer.translate(_("No discussion found with id=%s")) %
            discussion_id)

    discussion.import_from_sources()

    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(user_id, discussion_id)

    DEFAULT_PAGE_SIZE = 25
    page_size = DEFAULT_PAGE_SIZE

    filter_names = [
        filter_name for filter_name \
        in request.GET.getone('filters').split(',') \
        if filter_name
    ] if request.GET.get('filters') else []

    try:
        page = int(request.GET.getone('page'))
    except (ValueError, KeyError):
        page = 1

    text_search = request.GET.get('text_search', None)

    order = request.GET.get('order')
    if order == None:
        order = 'chronological'
    assert order in ('chronological', 'reverse_chronological', 'score')
    if order == 'score':
        assert text_search is not None

    if page < 1:
        page = 1

    root_post_id = request.GET.getall('root_post_id')
    if root_post_id:
        root_post_id = get_database_id("Post", root_post_id[0])
    family_post_id = request.GET.getall('family_post_id')
    if family_post_id:
        family_post_id = get_database_id("Post", family_post_id[0])

    root_idea_id = request.GET.getall('root_idea_id')
    if root_idea_id:
        root_idea_id = get_database_id("Idea", root_idea_id[0])

    ids = request.GET.getall('ids[]')
    if ids:
        ids = [get_database_id("Post", id) for id in ids]

    view_def = request.GET.get('view') or 'default'

    only_synthesis = request.GET.get('only_synthesis')

    post_author_id = request.GET.get('post_author')
    if post_author_id:
        post_author_id = get_database_id("AgentProfile", post_author_id)
        assert AgentProfile.get(
            post_author_id
        ), "Unable to find agent profile with id " + post_author_id

    post_replies_to = request.GET.get('post_replies_to')
    if post_replies_to:
        post_replies_to = get_database_id("AgentProfile", post_replies_to)
        assert AgentProfile.get(
            post_replies_to
        ), "Unable to find agent profile with id " + post_replies_to

    posted_after_date = request.GET.get('posted_after_date')
    posted_before_date = request.GET.get('posted_before_date')

    PostClass = SynthesisPost if only_synthesis == "true" else Post
    if order == 'score':
        posts = discussion.db.query(PostClass,
                                    Content.body_text_index.score_name)
    else:
        posts = discussion.db.query(PostClass)

    posts = posts.filter(PostClass.discussion_id == discussion_id, )
    ##no_of_posts_to_discussion = posts.count()

    post_data = []

    only_orphan = request.GET.get('only_orphan')
    if only_orphan == "true":
        if root_idea_id:
            raise HTTPBadRequest(
                localizer.translate(
                    _("Getting orphan posts of a specific idea isn't supported."
                      )))
        orphans = text(Idea._get_orphan_posts_statement(),
                       bindparams=[
                           bindparam('discussion_id', discussion_id)
                       ]).columns(column('post_id')).alias('orphans')
        posts = posts.join(orphans, PostClass.id == orphans.c.post_id)
    elif only_orphan == "false":
        raise HTTPBadRequest(
            localizer.translate(
                _("Getting non-orphan posts isn't supported.")))

    # "true" means hidden only, "false" (default) means visible only. "any" means both.
    hidden = request.GET.get('hidden_messages', "false")
    if hidden != 'any':
        posts = posts.filter(PostClass.hidden == asbool(hidden))

    if root_idea_id:
        related = text(Idea._get_related_posts_statement(),
                       bindparams=[
                           bindparam('root_idea_id', root_idea_id),
                           bindparam('discussion_id', discussion_id)
                       ]).columns(column('post_id')).alias('related')
        #Virtuoso bug: This should work...
        #posts = posts.join(related, PostClass.id==related.c.post_id)
        posts = posts.join(related, PostClass.id == related.c.post_id)
    if root_post_id:
        root_post = Post.get(root_post_id)

        posts = posts.filter((Post.ancestry.like(root_post.ancestry +
                                                 cast(root_post.id, String) +
                                                 ',%'))
                             | (PostClass.id == root_post.id))
    elif family_post_id:
        root_post = Post.get(family_post_id)
        ancestor_ids = root_post.ancestor_ids()
        posts = posts.filter((Post.ancestry.like(root_post.ancestry +
                                                 cast(root_post.id, String) +
                                                 ',%'))
                             | (PostClass.id == root_post.id)
                             | (PostClass.id.in_(ancestor_ids)))
    else:
        root_post = None

    if ids:
        posts = posts.filter(Post.id.in_(ids))

    if posted_after_date:
        posted_after_date = parse_datetime(posted_after_date)
        if posted_after_date:
            posts = posts.filter(PostClass.creation_date >= posted_after_date)
        #Maybe we should do something if the date is invalid.  benoitg

    if posted_before_date:
        posted_before_date = parse_datetime(posted_before_date)
        if posted_before_date:
            posts = posts.filter(PostClass.creation_date <= posted_before_date)
        #Maybe we should do something if the date is invalid.  benoitg

    if post_author_id:
        posts = posts.filter(PostClass.creator_id == post_author_id)

    if post_replies_to:
        parent_alias = aliased(PostClass)
        posts = posts.join(parent_alias, PostClass.parent)
        posts = posts.filter(parent_alias.creator_id == post_replies_to)
    # Post read/unread management
    is_unread = request.GET.get('is_unread')
    if user_id != Everyone:
        # This is horrible, but the join creates complex subqueries that
        # virtuoso cannot decode properly.
        read_posts = {
            v.post_id
            for v in discussion.db.query(ViewPost).filter(
                ViewPost.tombstone_condition(), ViewPost.actor_id == user_id,
                *ViewPost.get_discussion_conditions(discussion_id))
        }
        liked_posts = {
            l.post_id: l.id
            for l in discussion.db.query(LikedPost).filter(
                LikedPost.tombstone_condition(), LikedPost.actor_id == user_id,
                *LikedPost.get_discussion_conditions(discussion_id))
        }
        if is_unread != None:
            posts = posts.outerjoin(
                ViewPost,
                and_(ViewPost.actor_id == user_id,
                     ViewPost.post_id == PostClass.id,
                     ViewPost.tombstone_date == None))
            if is_unread == "true":
                posts = posts.filter(ViewPost.id == None)
            elif is_unread == "false":
                posts = posts.filter(ViewPost.id != None)
    else:
        #If there is no user_id, all posts are always unread
        if is_unread == "false":
            raise HTTPBadRequest(
                localizer.translate(
                    _("You must be logged in to view which posts are read")))

    if text_search is not None:
        # another Virtuoso bug: offband kills score. but it helps speed.
        offband = () if (order == 'score') else None
        posts = posts.filter(
            Post.body_text_index.contains(text_search.encode('utf-8'),
                                          offband=offband))

    #posts = posts.options(contains_eager(Post.source))
    # Horrible hack... But useful for structure load
    if view_def == 'id_only':
        pass  # posts = posts.options(defer(Post.body))
    else:
        posts = posts.options(joinedload_all(Post.creator))
        posts = posts.options(joinedload_all(Post.extracts))
        posts = posts.options(joinedload_all(Post.widget_idea_links))
        posts = posts.options(joinedload_all(
            SynthesisPost.publishes_synthesis))

    if order == 'chronological':
        posts = posts.order_by(Content.creation_date)
    elif order == 'reverse_chronological':
        posts = posts.order_by(Content.creation_date.desc())
    elif order == 'score':
        posts = posts.order_by(Content.body_text_index.score_name.desc())
    print str(posts)

    no_of_posts = 0
    no_of_posts_viewed_by_user = 0

    for query_result in posts:
        score, viewpost, likedpost = None, None, None
        if not isinstance(query_result, (list, tuple)):
            query_result = [query_result]
        post = query_result[0]
        if user_id != Everyone:
            viewpost = post.id in read_posts
            likedpost = liked_posts.get(post.id, None)
        no_of_posts += 1
        serializable_post = post.generic_json(view_def, user_id,
                                              permissions) or {}
        if order == 'score':
            score = query_result[1]
            serializable_post['score'] = score

        if viewpost:
            serializable_post['read'] = True
            no_of_posts_viewed_by_user += 1
        elif user_id != Everyone and root_post is not None and root_post.id == post.id:
            # Mark post read, we requested it explicitely
            viewed_post = ViewPost(actor_id=user_id, post=root_post)
            discussion.db.add(viewed_post)
            serializable_post['read'] = True
        else:
            serializable_post['read'] = False
        # serializable_post['liked'] = likedpost.uri() if likedpost else False
        serializable_post['liked'] = LikedPost.uri_generic(
            likedpost) if likedpost else False

        post_data.append(serializable_post)

    # Benoitg:  For now, this completely garbles threading without intelligent
    #handling of pagination.  Disabling
    #posts = posts.limit(page_size).offset(data['startIndex']-1)
    # This code isn't up to date.  If limiting the query by page, we need to
    # calculate the counts with a separate query to have the right number of
    # results
    #no_of_messages_viewed_by_user = discussion.db.query(ViewPost).join(
    #    Post
    #).filter(
    #    Post.discussion_id == discussion_id,
    #    ViewPost.actor_id == user_id,
    #).count() if user_id else 0

    data = {}
    data["page"] = page
    data["unread"] = no_of_posts - no_of_posts_viewed_by_user
    data["total"] = no_of_posts
    data["maxPage"] = max(1, ceil(float(data["total"]) / page_size))
    #TODO:  Check if we want 1 based index in the api
    data["startIndex"] = (page_size * page) - (page_size - 1)

    if data["page"] == data["maxPage"]:
        data["endIndex"] = data["total"]
    else:
        data["endIndex"] = data["startIndex"] + (page_size - 1)
    data["posts"] = post_data

    return data
Example #41
0
def create_post(request):
    """
    We use post, not put, because we don't know the id of the post
    """
    localizer = request.localizer
    request_body = json.loads(request.body)
    user_id = authenticated_userid(request)
    if not user_id:
        raise HTTPUnauthorized()

    user = Post.default_db.query(User).filter_by(id=user_id).one()

    message = request_body.get('message', None)
    html = request_body.get('html', None)
    reply_id = request_body.get('reply_id', None)
    idea_id = request_body.get('idea_id', None)
    subject = request_body.get('subject', None)
    publishes_synthesis_id = request_body.get('publishes_synthesis_id', None)

    if not message:
        raise HTTPBadRequest(localizer.translate(_("Your message is empty")))

    if reply_id:
        in_reply_to_post = Post.get_instance(reply_id)
    else:
        in_reply_to_post = None

    if idea_id:
        in_reply_to_idea = Idea.get_instance(idea_id)
    else:
        in_reply_to_idea = None

    discussion_id = int(request.matchdict['discussion_id'])
    discussion = Discussion.get_instance(discussion_id)

    if not discussion:
        raise HTTPNotFound(
            localizer.translate(
                _("No discussion found with id=%s" % discussion_id)))

    if subject:
        subject = subject
    else:
        #print(in_reply_to_post.subject, discussion.topic)
        if in_reply_to_post:
            subject = in_reply_to_post.get_title(
            ) if in_reply_to_post.get_title() else ''
        elif in_reply_to_idea:
            #TODO:  THis should use a cascade like the frontend
            subject = in_reply_to_idea.short_title if in_reply_to_idea.short_title else ''
        else:
            subject = discussion.topic if discussion.topic else ''
        #print subject
        subject = "Re: " + restrip_pat.sub('', subject)

    post_constructor_args = {
        'discussion': discussion,
        'message_id': uuid.uuid1().hex + "@" + config.get('public_hostname'),
        'creator_id': user_id,
        'subject': subject,
        'body': html if html else message
    }

    if publishes_synthesis_id:
        published_synthesis = Synthesis.get_instance(publishes_synthesis_id)
        post_constructor_args['publishes_synthesis'] = published_synthesis
        new_post = SynthesisPost(**post_constructor_args)
    else:
        new_post = AssemblPost(**post_constructor_args)

    discussion.db.add(new_post)
    discussion.db.flush()

    if in_reply_to_post:
        new_post.set_parent(in_reply_to_post)
    if in_reply_to_idea:
        idea_post_link = IdeaRelatedPostLink(creator_id=user_id,
                                             content=new_post,
                                             idea=in_reply_to_idea)
        discussion.db.add(idea_post_link)
    for source in discussion.sources:
        source.send_post(new_post)
    permissions = get_permissions(user_id, discussion_id)

    return new_post.generic_json('default', user_id, permissions)
Example #42
0
 def __call__(self, context, request):
     return isinstance(context, InstanceContext) and get_permissions(
         request.authenticated_userid, request.discussion_id, context._instance)
Example #43
0
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()}
Example #44
0
    def mutate(root, args, context, info):
        cls = models.Thematic
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(user_id, CrudPermissions.CREATE,
                                   permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        identifier = args.get('identifier')
        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            title_langstring = langstring_from_input_entries(title_entries)
            description_langstring = langstring_from_input_entries(
                args.get('description_entries'))
            kwargs = {}
            if description_langstring is not None:
                kwargs['description'] = description_langstring

            video = args.get('video')
            if video is not None:
                video_title = langstring_from_input_entries(
                    video['title_entries'])
                if video_title is not None:
                    kwargs['video_title'] = video_title

                video_description = langstring_from_input_entries(
                    video['description_entries'])
                if video_description is not None:
                    kwargs['video_description'] = video_description

                kwargs['video_html_code'] = video['html_code']

            # Our thematic, because it inherits from Idea, needs to be
            # associated to the root idea of the discussion.
            # We create a hidden root thematic, corresponding to the
            # `identifier` phase, child of the root idea,
            # and add our thematic as a child of this root thematic.
            root_thematic = [
                idea for idea in discussion.root_idea.get_children()
                if getattr(idea, 'identifier', '') == identifier
            ]
            if not root_thematic:
                short_title = u'Phase {}'.format(identifier)
                root_thematic = cls(discussion_id=discussion_id,
                                    short_title=short_title,
                                    title=langstring_from_input_entries([{
                                        'locale_code':
                                        'en',
                                        'value':
                                        short_title
                                    }]),
                                    identifier=identifier,
                                    hidden=True)
                discussion.root_idea.children.append(root_thematic)
            else:
                root_thematic = root_thematic[0]

            # take the first entry and set it for short_title
            short_title = title_entries[0]['value']
            saobj = cls(discussion_id=discussion_id,
                        title=title_langstring,
                        short_title=short_title,
                        identifier=identifier,
                        **kwargs)
            root_thematic.children.append(saobj)
            db = saobj.db
            db.add(saobj)
            db.flush()

            questions_input = args.get('questions')
            if questions_input is not None:
                for question_input in questions_input:
                    title_ls = langstring_from_input_entries(
                        question_input['title_entries'])
                    saobj.children.append(
                        models.Question(title=title_ls,
                                        discussion_id=discussion_id))
                db.flush()

        return CreateThematic(thematic=saobj)
Example #45
0
def extract_voters(request):
    extract_votes = []
    ctx = request.context
    user_id = request.authenticated_userid
    if not user_id:
        raise HTTPUnauthorized
    widget = ctx._instance
    user_id = request.authenticated_userid
    if widget.activity_state != "ended":
        permissions = get_permissions(user_id, ctx.get_discussion_id())
        if P_ADMIN_DISC not in permissions:
            raise HTTPUnauthorized()
    user_prefs = LanguagePreferenceCollection.getCurrent()
    fieldnames = [
        "Nom du contributeur", "Nom d'utilisateur du contributeur",
        "Adresse mail du contributeur", "Date/heure du vote", "Proposition"
    ]
    votes = widget.db.query(AbstractIdeaVote).filter(
        AbstractVoteSpecification.widget_id == widget.id).filter(
            AbstractIdeaVote.tombstone_date == None).order_by(
                AbstractIdeaVote.vote_spec_id.desc()).all()
    for count, vote in enumerate(votes):
        voter = vote.voter
        contributor = voter.real_name() or u""
        contributor_username = voter.username_p or u""
        contributor_mail = voter.get_preferred_email() or u""
        vote_date = vote.vote_date or u""
        proposition = Idea.get(
            vote.idea_id).title.best_lang(user_prefs).value or u""
        vote_value = vote.vote_value

        if votes[count].vote_spec_id != votes[
                count - 1].vote_spec_id and fieldnames[-1] != "  ":
            fieldnames.append("  ")

        extract_info = {
            "Nom du contributeur":
            contributor.encode('utf-8'),
            "Nom d'utilisateur du contributeur":
            contributor_username.encode('utf-8'),
            "Adresse mail du contributeur":
            contributor_mail.encode('utf-8'),
            "Date/heure du vote":
            str(vote_date),
            "Proposition":
            proposition.encode('utf-8'),
        }

        if vote.type == u'token_idea_vote':
            token_category = vote.token_category.name.best_lang(
                user_prefs).value or u""
            token_category_encoded = token_category.encode('utf-8')
            if token_category_encoded not in fieldnames:
                fieldnames.append(token_category_encoded)
            extract_info.update({token_category: str(vote_value)})
            extract_votes.append(extract_info)

        if vote.type == u'gauge_idea_vote':
            spec = vote.vote_spec
            if isinstance(spec, NumberGaugeVoteSpecification):
                for choice_value in range_float(spec.minimum, spec.maximum,
                                                spec.nb_ticks):
                    option = u"{} {}".format(choice_value,
                                             spec.unit).encode('utf-8')
                    if option not in fieldnames:
                        fieldnames.append(option)
                    extract_info.update(
                        {option: "1" if vote_value == choice_value else "0"})
            else:
                for choice in spec.get_choices():
                    option = choice.label.best_lang(user_prefs).value.encode(
                        'utf-8')
                    if option not in fieldnames:
                        fieldnames.append(option)
                    extract_info.update(
                        {option: "1" if vote_value == choice.value else "0"})

            extract_votes.append(extract_info)
    extract_votes.sort(key=operator.itemgetter('Nom du contributeur'))
    return csv_response(
        extract_votes,
        CSV_MIMETYPE,
        fieldnames,
        content_disposition='attachment; filename="detailed_vote_results.csv"')
Example #46
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        discussion_id = context.matchdict['discussion_id']

        user_id = context.authenticated_userid or Everyone
        discussion = models.Discussion.get(discussion_id)

        post_id = args.get('post_id')
        post_id = int(Node.from_global_id(post_id)[1])
        post = models.Post.get(post_id)
        cls = models.Post

        permissions = get_permissions(user_id, discussion_id)
        allowed = post.user_can(user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized()

        changed = False
        subject = args.get('subject')
        if subject:
            subject = sanitize_text(subject)
        body = args.get('body')
        if body:
            body = sanitize_html(body)
        # TODO: Here, an assumption that the modification uses the same
        # language as the original. May need revisiting.
        original_subject_entry = post.subject.first_original()
        # subject is not required, be careful to not remove it if not specified
        if subject and subject != original_subject_entry.value:
            changed = True
            post.subject.add_value(subject, original_subject_entry.locale_code)
            # Edit subject for all descendants
            children = post.children[:]
            new_subject = u'Re: ' + restrip_pat.sub('', subject).strip()
            while children:
                child = children.pop()
                children.extend(child.children)
                child.subject.add_value(
                    new_subject,
                    child.subject.first_original().locale_code)

        original_body_entry = post.body.first_original()
        if body != original_body_entry.value:
            post.body.add_value(body, original_body_entry.locale_code)
            changed = True

            original_attachments = post.attachments
            original_attachments_doc_ids = []
            if original_attachments:
                original_attachments_doc_ids = [
                    str(a.document_id) for a in original_attachments
                ]

            attachments = args.get('attachments', [])
            for document_id in attachments:
                if document_id not in original_attachments_doc_ids:
                    document = models.Document.get(document_id)
                    models.PostAttachment(
                        document=document,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        post=post,
                        title=document.title,
                        attachmentPurpose=EMBED_ATTACHMENT)

            # delete attachments that has been removed
            documents_to_delete = set(original_attachments_doc_ids) - set(
                attachments)  # noqa: E501
            for document_id in documents_to_delete:
                with cls.default_db.no_autoflush:
                    document = models.Document.get(document_id)
                    post_attachment = post.db.query(
                        models.PostAttachment).filter_by(
                            discussion_id=discussion_id,
                            post_id=post_id,
                            document_id=document_id).first()
                    document.delete_file()
                    post.db.delete(document)
                    post.attachments.remove(post_attachment)
                    post.db.flush()

        publication_state = models.PublicationStates.from_string(
            args.get('publication_state')) if args.get(
                'publication_state') in models.PublicationStates.values(
                ) else None
        if publication_state and publication_state != post.publication_state:
            post.publication_state = publication_state
            changed = True
            # Update the creation date when switching from draft to published
            if post.publication_state == models.PublicationStates.DRAFT and publication_state == models.PublicationStates.PUBLISHED:
                post.creation_date = datetime.utcnow()

        if changed:
            post.modification_date = datetime.utcnow()
            post.body_mime_type = u'text/html'
            post.db.flush()
            post.db.expire(post.subject, ["entries"])
            post.db.expire(post.body, ["entries"])

        return UpdatePost(post=post)
Example #47
0
def create_post(request):
    """
    Create a new post in this discussion.

    We use post, not put, because we don't know the id of the post
    """
    localizer = request.localizer
    request_body = json.loads(request.body)
    user_id = authenticated_userid(request)
    if not user_id:
        raise HTTPUnauthorized()

    user = Post.default_db.query(User).filter_by(id=user_id).one()

    body = request_body.get('body', None)
    html = request_body.get('html',
                            None)  # BG: Is this used now? I cannot see it.
    reply_id = request_body.get('reply_id', None)
    idea_id = request_body.get('idea_id', None)
    subject = request_body.get('subject', None)
    publishes_synthesis_id = request_body.get('publishes_synthesis_id', None)
    message_classifier = request_body.get('message_classifier', None)

    if not body and not publishes_synthesis_id:
        # Should we allow empty messages otherwise?
        raise HTTPBadRequest(localizer.translate(_("Your message is empty")))

    if reply_id:
        in_reply_to_post = Post.get_instance(reply_id)
    else:
        in_reply_to_post = None

    if idea_id:
        in_reply_to_idea = Idea.get_instance(idea_id)
    else:
        in_reply_to_idea = None

    discussion_id = int(request.matchdict['discussion_id'])
    discussion = Discussion.get_instance(discussion_id)

    if not discussion:
        raise HTTPNotFound(
            localizer.translate(_("No discussion found with id=%s")) %
            (discussion_id, ))

    ctx = DummyContext({Discussion: discussion})
    if html:
        log.warning("Still using html")
        # how to guess locale in this case?
        body = LangString.create(html)
    elif body:
        body = LangString.create_from_json(body, context=ctx, user_id=user_id)
    else:
        body = LangString.EMPTY(discussion.db)

    if subject:
        subject = LangString.create_from_json(subject,
                                              context=ctx,
                                              user_id=user_id)
    else:
        # print(in_reply_to_post.subject, discussion.topic)
        if in_reply_to_post:
            subject = (in_reply_to_post.get_title().first_original().value
                       or '' if in_reply_to_post.get_title() else '')
        elif in_reply_to_idea:
            # TODO:  THis should use a cascade like the frontend
            subject = (in_reply_to_idea.short_title
                       if in_reply_to_idea.short_title else '')
        else:
            subject = discussion.topic if discussion.topic else ''
        # print subject
        if subject is not None and len(subject):
            new_subject = "Re: " + restrip_pat.sub('', subject).strip()
            if (in_reply_to_post and new_subject == subject
                    and in_reply_to_post.get_title()):
                # reuse subject and translations
                subject = in_reply_to_post.get_title().clone(discussion.db)
            else:
                # how to guess locale in this case?
                subject = LangString.create(new_subject)
        else:
            capture_message(
                "A message is about to be written to the database with an "
                "empty subject.  This is not supposed to happen.")
            subject = LangString.EMPTY(discussion.db)

    post_constructor_args = {
        'discussion': discussion,
        'creator_id': user_id,
        'message_classifier': message_classifier,
        'subject': subject,
        'body': body
    }

    if publishes_synthesis_id:
        published_synthesis = Synthesis.get_instance(publishes_synthesis_id)
        post_constructor_args['publishes_synthesis'] = published_synthesis
        new_post = SynthesisPost(**post_constructor_args)
        new_post.finalize_publish()
    else:
        new_post = AssemblPost(**post_constructor_args)

    discussion.db.add(new_post)
    discussion.db.flush()

    if in_reply_to_post:
        new_post.set_parent(in_reply_to_post)
    if in_reply_to_idea:
        idea_post_link = IdeaRelatedPostLink(creator_id=user_id,
                                             content=new_post,
                                             idea=in_reply_to_idea)
        discussion.db.add(idea_post_link)
        idea = in_reply_to_idea
        while idea:
            idea.send_to_changes()
            parents = idea.get_parents()
            idea = next(iter(parents)) if parents else None
    else:
        discussion.root_idea.send_to_changes()
    for source in discussion.sources:
        if 'send_post' in dir(source):
            source.send_post(new_post)
    permissions = get_permissions(user_id, discussion_id)

    return new_post.generic_json('default', user_id, permissions)
Example #48
0
    def mutate(root, args, context, info):
        cls = models.User
        db = cls.default_db
        discussion_id = context.matchdict['discussion_id']
        user_id = context.authenticated_userid or Everyone
        instance = cls.get(user_id) if user_id != Everyone else None
        require_instance_permission(CrudPermissions.UPDATE, instance, context)

        permissions = get_permissions(user_id, discussion_id)
        from assembl.models.action import (
            RejectCGUOnDiscussion, RejectSessionOnDiscussion, RejectTrackingOnDiscussion, RejectPrivacyPolicyOnDiscussion,
            AcceptCGUOnDiscussion, AcceptSessionOnDiscussion, AcceptTrackingOnDiscussion, AcceptPrivacyPolicyOnDiscussion,
            AcceptUserGuidelineOnDiscussion, RejectUserGuidelineOnDiscussion
        )
        with cls.default_db.no_autoflush as db:
            user = models.User.get(user_id)
            actions = args.get('actions', [])
            agent_status_in_discussion = user.get_status_in_discussion(discussion_id)
            for action_type in actions:
                action_type_enum = PyCookieTypes(action_type)
                if action_type_enum == PyCookieTypes.ACCEPT_CGU:
                    action = AcceptCGUOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_accepted_cgu_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_CGU)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.ACCEPT_SESSION_ON_DISCUSSION:
                    action = AcceptSessionOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_SESSION_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.ACCEPT_TRACKING_ON_DISCUSSION:
                    action = AcceptTrackingOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_TRACKING_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.ACCEPT_PRIVACY_POLICY_ON_DISCUSSION:
                    action = AcceptPrivacyPolicyOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_accepted_privacy_policy_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_PRIVACY_POLICY_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.ACCEPT_LOCALE:
                    action = AcceptPrivacyPolicyOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_accepted_privacy_policy_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_LOCALE)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.ACCEPT_USER_GUIDELINE_ON_DISCUSSION:
                    action = AcceptUserGuidelineOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.last_accepted_user_guideline_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.REJECT_USER_GUIDELINE_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_CGU:
                    action = RejectCGUOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_rejected_cgu_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_CGU)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_SESSION_ON_DISCUSSION:
                    action = RejectSessionOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_SESSION_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_TRACKING_ON_DISCUSSION:
                    action = RejectTrackingOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_TRACKING_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_PRIVACY_POLICY_ON_DISCUSSION:
                    action = RejectPrivacyPolicyOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_rejected_privacy_policy_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_PRIVACY_POLICY_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_LOCALE:
                    action = AcceptPrivacyPolicyOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.user_last_accepted_privacy_policy_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_LOCALE)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                elif action_type_enum == PyCookieTypes.REJECT_USER_GUIDELINE_ON_DISCUSSION:
                    action = RejectUserGuidelineOnDiscussion(discussion_id=discussion_id, actor_id=user_id)
                    user.last_rejected_user_guideline_date = datetime.utcnow()
                    agent_status_in_discussion.delete_cookie(PyCookieTypes.ACCEPT_USER_GUIDELINE_ON_DISCUSSION)
                    agent_status_in_discussion.update_cookie(action_type_enum)

                action = action.handle_duplication(permissions=permissions, user_id=user.id)
                db.add(action)
        db.flush()
        return UpdateAcceptedCookies(user=user)
Example #49
0
def assembl_register_view(request):
    slug = request.matchdict.get('discussion_slug', "")
    next_view = handle_next_view(request)
    if not request.params.get('email'):
        if request.scheme == "http"\
                and asbool(config.get("accept_secure_connection")):
            return HTTPFound(get_global_base_url(True) + request.path_qs)
        response = get_login_context(request)
        return response
    forget(request)
    session = AgentProfile.default_db
    localizer = request.localizer
    name = request.params.get('name', '').strip()
    if not name or len(name) < 3:
        return dict(get_default_context(request),
            error=localizer.translate(_(
                "Please use a name of at least 3 characters")))
    password = request.params.get('password', '').strip()
    password2 = request.params.get('password2', '').strip()
    email = request.params.get('email', '').strip()
    if not is_email(email):
        return dict(get_default_context(request),
                    error=localizer.translate(_(
                        "This is not a valid email")))
    email = EmailString.normalize_email_case(email)
    # Find agent account to avoid duplicates!
    if session.query(AbstractAgentAccount).filter_by(
            email_ci=email, verified=True).count():
        return dict(get_default_context(request),
                    error=localizer.translate(_(
                        "We already have a user with this email.")))
    if password != password2:
        return dict(get_default_context(request),
                    error=localizer.translate(_(
                        "The passwords should be identical")))

    # TODO: Validate password quality
    # otherwise create.
    validate_registration = asbool(config.get(
        'assembl.validate_registration_emails'))

    user = User(
        name=name,
        password=password,
        verified=not validate_registration,
        creation_date=datetime.utcnow()
    )
    email_account = EmailAccount(
        email=email,
        verified=not validate_registration,
        profile=user
    )
    session.add(user)
    session.add(email_account)
    discussion = discussion_from_request(request)
    if discussion:
        permissions = get_permissions(Everyone, discussion.id)
        if not (P_SELF_REGISTER in permissions or
                P_SELF_REGISTER_REQUEST in permissions):
            discussion = None
    if discussion:
        _now = datetime.utcnow()
        agent_status = AgentStatusInDiscussion(
            agent_profile=user, discussion=discussion,
            first_visit=_now, last_visit=_now,
            user_created_on_this_discussion=True)
        session.add(agent_status)
    session.flush()
    if not validate_registration:
        if asbool(config.get('pyramid.debug_authorization')):
            # for debugging purposes
            from assembl.auth.password import email_token
            print "email token:", request.route_url(
                'user_confirm_email', token=email_token(email_account))
        headers = remember(request, user.id)
        user.last_login = datetime.utcnow()
        request.response.headerlist.extend(headers)
        if discussion:
            maybe_auto_subscribe(user, discussion)
        # TODO: Tell them to expect an email.
        return HTTPFound(location=next_view)
    return HTTPFound(location=maybe_contextual_route(
        request, 'confirm_emailid_sent', email_account_id=email_account.id))
Example #50
0
def instance_put(request):
    ctx = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(user_id, ctx.get_discussion_id())
    instance = ctx._instance
    if not instance.user_can(user_id, CrudPermissions.UPDATE, permissions):
        return HTTPUnauthorized()
    mapper = inspect(instance.__class__)
    cols = {c.key: c for c in mapper.columns if not c.foreign_keys}
    setables = dict(
        pyinspect.getmembers(
            instance.__class__,
            lambda p: pyinspect.isdatadescriptor(p) and getattr(
                p, 'fset', None)))
    relns = {
        r.key: r
        for r in mapper.relationships
        if not r.uselist and len(r._calculated_foreign_keys) == 1
        and iter(r._calculated_foreign_keys).next().table == mapper.local_table
    }
    unknown = set(request.params.keys()) - (set(cols.keys()).union(
        set(setables.keys())).union(set(relns.keys())))
    if unknown:
        raise HTTPBadRequest("Unknown keys: " + ",".join(unknown))
    params = dict(request.params)
    # type checking
    columns = {c.key: c for c in mapper.columns}
    for key, value in params.items():
        if key in relns and isinstance(value, (str, unicode)):
            val_inst = relns[key].class_.get_instance(value)
            if not val_inst:
                raise HTTPBadRequest("Unknown instance: " + value)
            params[key] = val_inst
        elif key in columns and isinstance(columns[key].type, DeclEnumType) \
                and isinstance(value, (str, unicode)):
            val_det = columns[key].type.enum.from_string(value)
            if not val_det:
                raise HTTPBadRequest("Cannot interpret " + value)
            params[key] = val_det
        elif key in columns and columns[key].type.python_type == datetime.datetime \
                and isinstance(value, (str, unicode)):
            val_dt = datetime.datetime.strpstr(value)
            if not val_dt:
                raise HTTPBadRequest("Cannot interpret " + value)
            params[key] = val_dt
        elif key in columns and columns[key].type.python_type == int \
                and isinstance(value, (str, unicode)):
            try:
                params[key] = int(value)
            except ValueError as err:
                raise HTTPBadRequest("Not a number: " + value)
        elif key in columns and not isinstance(value,
                                               columns[key].type.python_type):
            raise HTTPBadRequest("Value %s for key %s should be a %s" %
                                 (value, key, columns[key].type.python_type))
    try:
        for key, value in params.items():
            setattr(instance, key, value)
    except:
        raise HTTPBadRequest()
    view = request.GET.get('view', None) or 'default'
    if view == 'id_only':
        return [instance.uri()]
    else:
        user_id = authenticated_userid(request) or Everyone
        return instance.generic_json(view, user_id, permissions)
Example #51
0
    def mutate(root, args, context, info):
        cls = models.Discussion
        discussion_id = context.matchdict['discussion_id']
        discussion = cls.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = discussion.user_can(
            user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush as db:
            title_entries = args.get('title_entries')
            if title_entries is not None and len(title_entries) == 0:
                raise Exception(
                    'Title entries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(
                discussion, 'title', title_entries)

            subtitle_entries = args.get('subtitle_entries')
            update_langstring_from_input_entries(
                discussion, 'subtitle', subtitle_entries)

            button_label_entries = args.get('button_label_entries')
            update_langstring_from_input_entries(
                discussion, 'button_label', button_label_entries)

            # add uploaded header image as an attachment to the discussion
            LANDING_PAGE_HEADER_IMAGE = models.AttachmentPurpose.LANDING_PAGE_HEADER_IMAGE.value
            image = args.get('header_image')
            if image is not None:
                header_images = [
                    att for att in discussion.attachments
                    if att.attachmentPurpose == LANDING_PAGE_HEADER_IMAGE
                ]

                if image == 'TO_DELETE' and header_images:
                    header_image = header_images[0]
                    header_image.document.delete_file()
                    db.delete(header_image.document)
                    discussion.attachments.remove(header_image)
                else:
                    filename = os.path.basename(context.POST[image].filename)
                    mime_type = context.POST[image].type
                    document = models.File(
                        discussion=discussion,
                        mime_type=mime_type,
                        title=filename)
                    document.add_file_data(context.POST[image].file)

                    # if there is already an IMAGE, remove it with the
                    # associated document
                    if header_images:
                        header_image = header_images[0]
                        header_image.document.delete_file()
                        db.delete(header_image.document)
                        discussion.attachments.remove(header_image)

                    db.add(models.DiscussionAttachment(
                        document=document,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        title=filename,
                        attachmentPurpose=LANDING_PAGE_HEADER_IMAGE
                    ))

            # add uploaded logo image as an attachment to the discussion
            LANDING_PAGE_LOGO_IMAGE = models.AttachmentPurpose.LANDING_PAGE_LOGO_IMAGE.value
            image = args.get('logo_image')
            if image is not None:
                logo_images = [
                    att for att in discussion.attachments
                    if att.attachmentPurpose == LANDING_PAGE_LOGO_IMAGE
                ]

                if image == 'TO_DELETE' and logo_images:
                    logo_image = logo_images[0]
                    logo_image.document.delete_file()
                    db.delete(logo_image.document)
                    discussion.attachments.remove(logo_image)
                else:
                    filename = os.path.basename(context.POST[image].filename)
                    mime_type = context.POST[image].type
                    document = models.File(
                        discussion=discussion,
                        mime_type=mime_type,
                        title=filename)
                    document.add_file_data(context.POST[image].file)

                    # if there is already an IMAGE, remove it with the
                    # associated document
                    if logo_images:
                        logo_image = logo_images[0]
                        logo_image.document.delete_file()
                        db.delete(logo_image.document)
                        discussion.attachments.remove(logo_image)

                    db.add(models.DiscussionAttachment(
                        document=document,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        title=filename,
                        attachmentPurpose=LANDING_PAGE_LOGO_IMAGE
                    ))

        db.flush()
        discussion = cls.get(discussion_id)
        return UpdateDiscussion(discussion=discussion)
Example #52
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        discussion_id = context.matchdict['discussion_id']

        user_id = context.authenticated_userid or Everyone
        discussion = models.Discussion.get(discussion_id)

        idea_id = args.get('idea_id')
        idea_id = int(Node.from_global_id(idea_id)[1])
        in_reply_to_idea = models.Idea.get(idea_id)

        if isinstance(in_reply_to_idea, models.Question):
            cls = models.PropositionPost
        else:
            cls = models.AssemblPost

        extract_id = args.get('extract_id')
        if extract_id:
            extract_id_global = int(Node.from_global_id(extract_id)[1])
            extract = models.Extract.get(extract_id_global)
            cls = models.ExtractComment

        in_reply_to_post = None
        if (cls == models.AssemblPost) or (cls == models.ExtractComment):
            in_reply_to_post_id = args.get('parent_id')
            if in_reply_to_post_id:
                in_reply_to_post_id = int(
                    Node.from_global_id(in_reply_to_post_id)[1])
                if in_reply_to_post_id:
                    in_reply_to_post = models.Post.get(in_reply_to_post_id)

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(user_id, CrudPermissions.CREATE,
                                   permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            subject = args.get('subject')
            body = args.get('body')
            classifier = args.get('message_classifier', None)
            body = sanitize_html(body)
            body_langstring = models.LangString.create(body)
            publication_state = models.PublicationStates.from_string(
                args.get('publication_state')) if args.get(
                    'publication_state') in models.PublicationStates.values(
                    ) else models.PublicationStates.PUBLISHED

            if subject:
                subject = sanitize_text(subject)
                subject_langstring = models.LangString.create(subject)
            elif issubclass(cls, models.PropositionPost):
                # Specific case first. Respect inheritance. Since we are using
                # a specific value, construct it with localization machinery.
                subject_langstring = models.LangString.create_localized_langstring(  # noqa: E501
                    _('Proposal'), discussion.discussion_locales,
                    {'fr': 'Proposition'})
            else:
                # We apply the same logic than in views/api/post.py::create_post  # noqa: E501
                locale = models.Locale.UNDEFINED
                if in_reply_to_post and in_reply_to_post.get_title():
                    original_subject = in_reply_to_post.get_title(
                    ).first_original()
                    locale = original_subject.locale_code
                    subject = original_subject.value
                elif in_reply_to_idea:
                    # TODO: some ideas have extra langstring titles
                    # we try to guess the locale of the body to use the same locale for post's subject
                    body_lang, data = discussion.translation_service(
                    ).identify(body_langstring.entries[0].value,
                               discussion.discussion_locales)

                    closest_subject = in_reply_to_idea.title.closest_entry(
                        body_lang)
                    if closest_subject:
                        subject = closest_subject.value
                        locale = closest_subject.locale.code
                    else:
                        # rather no subject than one in a random locale
                        subject = u''
                        locale = discussion.main_locale
                else:
                    subject = discussion.topic if discussion.topic else ''
                    locale = discussion.main_locale

                if subject is not None:
                    if in_reply_to_idea and in_reply_to_idea.message_view_override == u'messageColumns':
                        new_subject = subject
                    else:
                        new_subject = u'Re: ' + restrip_pat.sub(
                            '', subject).strip()  # noqa: E501

                    if (in_reply_to_post and new_subject == subject
                            and in_reply_to_post.get_title()):
                        # reuse subject and translations
                        subject_langstring = in_reply_to_post.get_title(
                        ).clone(discussion.db)
                    else:
                        subject_langstring = models.LangString.create(
                            new_subject, locale)

            if cls == models.ExtractComment:
                new_post = cls(discussion=discussion,
                               subject=subject_langstring,
                               body=body_langstring,
                               creator_id=user_id,
                               body_mime_type=u'text/html',
                               message_classifier=classifier,
                               creation_date=datetime.utcnow(),
                               publication_state=publication_state,
                               parent_extract_id=extract.id)
            else:
                new_post = cls(discussion=discussion,
                               subject=subject_langstring,
                               body=body_langstring,
                               creator_id=user_id,
                               body_mime_type=u'text/html',
                               message_classifier=classifier,
                               creation_date=datetime.utcnow(),
                               publication_state=publication_state)

            new_post.guess_languages()
            db = new_post.db
            db.add(new_post)
            db.flush()

            if in_reply_to_post:
                new_post.set_parent(in_reply_to_post)
            elif in_reply_to_idea and cls != models.ExtractComment:
                # don't create IdeaRelatedPostLink when we have both
                # in_reply_to_post and in_reply_to_idea or if it's a comment
                # for an extract
                idea_post_link = models.IdeaRelatedPostLink(
                    creator_id=user_id,
                    content=new_post,
                    idea=in_reply_to_idea)
                db.add(idea_post_link)
            db.flush()

            attachments = args.get('attachments', [])
            for document_id in attachments:
                document = models.Document.get(document_id)
                models.PostAttachment(document=document,
                                      discussion=discussion,
                                      creator_id=context.authenticated_userid,
                                      post=new_post,
                                      title=document.title,
                                      attachmentPurpose=EMBED_ATTACHMENT)

            db.flush()

        return CreatePost(post=new_post)
Example #53
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        cls = models.Idea
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(user_id, CrudPermissions.CREATE,
                                   permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception('Idea titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            title_langstring = langstring_from_input_entries(title_entries)
            description_langstring = langstring_from_input_entries(
                args.get('description_entries'))
            kwargs = {}
            if description_langstring is not None:
                kwargs['description'] = description_langstring

            parent_idea_id = args.get('parent_id')
            if parent_idea_id:
                parent_idea_id = int(Node.from_global_id(parent_idea_id)[1])
                if parent_idea_id:
                    parent_idea = models.Idea.get(parent_idea_id)
                    if not parent_idea:
                        raise Exception('Parent Idea not found')
                    if parent_idea.discussion != discussion:
                        # No cross-debate references are allowed,
                        # for security reasons
                        raise Exception(
                            'Parent Idea does not belong to this discussion'
                        )  # noqa: E501
                else:
                    raise Exception('Parent Idea not found')
            if not parent_idea_id:
                parent_idea = discussion.root_idea

            saobj = cls(discussion_id=discussion_id,
                        title=title_langstring,
                        **kwargs)
            db = saobj.db
            db.add(saobj)
            order = len(parent_idea.get_children()) + 1.0
            db.add(
                models.IdeaLink(source=parent_idea,
                                target=saobj,
                                order=args.get('order', order)))

            # add uploaded image as an attachment to the idea
            image = args.get('image')
            if image is not None:
                filename = os.path.basename(context.POST[image].filename)
                mime_type = context.POST[image].type
                document = models.File(discussion=discussion,
                                       mime_type=mime_type,
                                       title=filename)
                document.add_file_data(context.POST[image].file)
                db.add(
                    models.IdeaAttachment(
                        document=document,
                        idea=saobj,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        title=filename,
                        attachmentPurpose=EMBED_ATTACHMENT))

            db.flush()

        return CreateIdea(idea=saobj)
Example #54
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        cls = models.Thematic
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        permissions = get_permissions(user_id, discussion_id)
        allowed = cls.user_can_cls(user_id, CrudPermissions.CREATE,
                                   permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        identifier = args.get('identifier')
        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            title_langstring = langstring_from_input_entries(title_entries)
            description_langstring = langstring_from_input_entries(
                args.get('description_entries'))
            kwargs = {}
            if description_langstring is not None:
                kwargs['description'] = description_langstring

            video = args.get('video')
            if video is not None:
                video_title = langstring_from_input_entries(
                    video.get('title_entries', None))
                if video_title is not None:
                    kwargs['video_title'] = video_title

                video_description_top = langstring_from_input_entries(
                    video.get('description_entries_top', None))
                if video_description_top is not None:
                    kwargs['video_description_top'] = video_description_top

                video_description_bottom = langstring_from_input_entries(
                    video.get('description_entries_bottom', None))
                if video_description_bottom is not None:
                    kwargs[
                        'video_description_bottom'] = video_description_bottom

                video_description_side = langstring_from_input_entries(
                    video.get('description_entries_side', None))
                if video_description_side is not None:
                    kwargs['video_description_side'] = video_description_side

                video_html_code = video.get('html_code', None)
                if video_html_code is not None:
                    kwargs['video_html_code'] = video_html_code

            # Our thematic, because it inherits from Idea, needs to be
            # associated to the root idea of the discussion.
            # We create a hidden root thematic, corresponding to the
            # `identifier` phase, child of the root idea,
            # and add our thematic as a child of this root thematic.
            root_thematic = get_root_thematic_for_phase(discussion, identifier)
            if root_thematic is None:
                root_thematic = create_root_thematic(discussion, identifier)

            saobj = cls(discussion_id=discussion_id,
                        title=title_langstring,
                        identifier=identifier,
                        **kwargs)
            db = saobj.db
            db.add(saobj)
            order = len(root_thematic.get_children()) + 1.0
            db.add(
                models.IdeaLink(source=root_thematic,
                                target=saobj,
                                order=args.get('order', order)))

            # add uploaded image as an attachment to the idea
            image = args.get('image')
            if image is not None:
                filename = os.path.basename(context.POST[image].filename)
                mime_type = context.POST[image].type
                document = models.File(discussion=discussion,
                                       mime_type=mime_type,
                                       title=filename)
                document.add_file_data(context.POST[image].file)
                db.add(
                    models.IdeaAttachment(
                        document=document,
                        idea=saobj,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        title=filename,
                        attachmentPurpose=EMBED_ATTACHMENT))

            db.flush()

            questions_input = args.get('questions')
            if questions_input is not None:
                for idx, question_input in enumerate(questions_input):
                    title_ls = langstring_from_input_entries(
                        question_input['title_entries'])
                    question = models.Question(title=title_ls,
                                               discussion_id=discussion_id)
                    db.add(
                        models.IdeaLink(source=saobj,
                                        target=question,
                                        order=idx + 1.0))
                db.flush()

        return CreateThematic(thematic=saobj)
Example #55
0
def assembl_register_user(request):
    forget(request)
    localizer = request.localizer
    session = AgentProfile.default_db
    json = request.json
    discussion = discussion_from_request(request)
    permissions = get_permissions(Everyone,
                                  discussion.id if discussion else None)

    if discussion and not (P_SELF_REGISTER in permissions
                           or P_SELF_REGISTER_REQUEST in permissions):
        # Consider it without context
        discussion = None

    name = json.get('real_name', '').strip()
    errors = JSONError()
    if not name or len(name) < 3:
        errors.add_error(
            localizer.translate(
                _("Please use a name of at least 3 characters")),
            ErrorTypes.SHORT_NAME)
    password = json.get('password', '').strip()
    # TODO: Check password strength. maybe pwdmeter?
    email = None
    for account in json.get('accounts', ()):
        email = account.get('email', None)
        if not is_email(email):
            errors.add_error(
                localizer.translate(_("This is not a valid email")),
                ErrorTypes.INVALID_EMAIL)
            continue
        email = EmailString.normalize_email_case(email)
        # Find agent account to avoid duplicates!
        if session.query(AbstractAgentAccount).filter_by(
                email_ci=email, verified=True).count():
            errors.add_error(
                localizer.translate(
                    _("We already have a user with this email.")),
                ErrorTypes.EXISTING_EMAIL, HTTPConflict.code)
    if not email:
        errors.add_error(localizer.translate(_("No email.")),
                         ErrorTypes.INVALID_EMAIL)
    username = json.get('username', None)
    if username:
        if session.query(Username).filter_by(username=username).count():
            errors.add_error(
                localizer.translate(
                    _("We already have a user with this username.")),
                ErrorTypes.EXISTING_USERNAME, HTTPConflict.code)

    if errors:
        raise errors

    validate_registration = asbool(
        config.get('assembl.validate_registration_emails'))

    old_autoflush = session.autoflush
    session.autoflush = False
    try:
        now = datetime.utcnow()
        user = User(name=name,
                    password=password,
                    verified=not validate_registration,
                    creation_date=now)

        session.add(user)
        session.flush()

        user.update_from_json(json, user_id=user.id)
        account = user.accounts[0]
        email = account.email
        account.verified = not validate_registration
        if discussion:
            agent_status = AgentStatusInDiscussion(
                agent_profile=user,
                discussion=discussion,
                first_visit=now,
                last_visit=now,
                user_created_on_this_discussion=True)
            session.add(agent_status)
        session.flush()

        if validate_registration:
            send_confirmation_email(request, account)
        else:
            user.verified = True
            for account in user.accounts:
                account.verified = True
            if asbool(config.get('pyramid.debug_authorization')):
                # for debugging purposes
                from assembl.auth.password import email_token
                print "email token:", request.route_url(
                    'user_confirm_email', token=email_token(account))
            if discussion:
                maybe_auto_subscribe(user, discussion)
        session.flush()
        return CreationResponse(user, Everyone, permissions)
    finally:
        session.autoflush = old_autoflush
Example #56
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        cls = models.Thematic
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        thematic_id = args.get('id')
        id_ = int(Node.from_global_id(thematic_id)[1])
        thematic = cls.get(id_)

        permissions = get_permissions(user_id, discussion_id)
        allowed = thematic.user_can(user_id, CrudPermissions.UPDATE,
                                    permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            # introducing history at every step, including thematics + questions  # noqa: E501
            thematic.copy(tombstone=True)
            title_entries = args.get('title_entries')
            if title_entries is not None and len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(thematic, 'title',
                                                 title_entries)
            update_langstring_from_input_entries(
                thematic, 'description', args.get('description_entries'))
            kwargs = {}
            video = args.get('video', None)
            if video is not None:
                update_langstring_from_input_entries(
                    thematic, 'video_title', video.get('title_entries', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_top',
                    video.get('description_entries_top', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_bottom',
                    video.get('description_entries_bottom', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_side',
                    video.get('description_entries_side', []))
                kwargs['video_html_code'] = video.get('html_code', None)

            if args.get('identifier') is not None:
                kwargs['identifier'] = args.get('identifier')

            for attr, value in kwargs.items():
                setattr(thematic, attr, value)

            db = thematic.db

            # change order if needed
            order = args.get('order')
            if order:
                thematic.source_links[0].order = order

            # add uploaded image as an attachment to the idea
            image = args.get('image')
            if image is not None:
                if image == 'TO_DELETE' and thematic.attachments:
                    # delete the image
                    attachment = thematic.attachments[0]
                    attachment.document.delete_file()
                    db.delete(attachment.document)
                    db.delete(attachment)
                    thematic.attachments.remove(attachment)
                else:
                    filename = os.path.basename(context.POST[image].filename)
                    mime_type = context.POST[image].type
                    document = models.File(discussion=discussion,
                                           mime_type=mime_type,
                                           title=filename)
                    document.add_file_data(context.POST[image].file)
                    # if there is already an attachment, remove it with the
                    # associated document (image)
                    if thematic.attachments:
                        thematic.attachments[0].document.delete_file()
                        db.delete(thematic.attachments[0].document)
                        thematic.attachments.remove(thematic.attachments[0])

                    attachment = models.IdeaAttachment(
                        document=document,
                        discussion=discussion,
                        creator_id=context.authenticated_userid,
                        title=filename,
                        attachmentPurpose=EMBED_ATTACHMENT)
                    thematic.attachments.append(attachment)
            db.flush()

            questions_input = args.get('questions')
            existing_questions = {
                question.id: question
                for question in thematic.get_children()
            }
            updated_questions = set()
            if questions_input is not None:
                for idx, question_input in enumerate(questions_input):
                    if question_input.get('id', None) is not None:
                        id_ = int(Node.from_global_id(question_input['id'])[1])
                        updated_questions.add(id_)
                        question = models.Question.get(id_)
                        # archive the question
                        question.copy(tombstone=True)
                        update_langstring_from_input_entries(
                            question, 'title', question_input['title_entries'])
                        # modify question order
                        question.source_links[0].order = idx + 1.0
                    else:
                        title_ls = langstring_from_input_entries(
                            question_input['title_entries'])
                        question = models.Question(title=title_ls,
                                                   discussion_id=discussion_id)
                        db.add(
                            models.IdeaLink(source=thematic,
                                            target=question,
                                            order=idx + 1.0))

                # remove question (tombstone) that are not in questions_input
                for question_id in set(existing_questions.keys()).difference(
                        updated_questions):
                    existing_questions[question_id].is_tombstone = True

            db.flush()

        return UpdateThematic(thematic=thematic)
Example #57
0
def search_endpoint(context, request):
    if not indexing_active():
        return HTTPServiceUnavailable("Indexing inactive")

    query = request.json_body
    # u'query': {u'bool': {u'filter': [{u'term': {u'discussion_id': u'23'}}]}}
    filters = [fil for fil in query['query']['bool']['filter']]
    discussion_id = [
        f.values()[0].values()[0] for f in filters
        if 'discussion_id' in f.values()[0].keys()
    ][0]
    discussion = models.Discussion.get_instance(discussion_id)
    if discussion is None:
        raise HTTPUnauthorized()

    user_id = authenticated_userid(request) or Everyone
    permissions = get_permissions(user_id, discussion_id)
    if not discussion.user_can(user_id, CrudPermissions.READ, permissions):
        raise HTTPUnauthorized()

    es = connect()
    index_name = get_index_settings()['index_name']
    #    print get_curl_query(query)
    result = es.search(index=index_name, body=query)

    # add creator_name in each hit
    creator_ids = set([
        hit['_source']['creator_id'] for hit in result['hits']['hits']
        if hit['_source'].get('creator_id', None) is not None
    ])
    session = get_session_maker()
    creators = session.query(
        models.AgentProfile.id, models.AgentProfile.name).filter(
            models.AgentProfile.id.in_(creator_ids)).all()
    creators_by_id = dict(creators)
    for hit in result['hits']['hits']:
        source = hit['_source']
        creator_id = source.get('creator_id', None)
        # Remove inner_hits key to not leak posts from private discussion.
        # You can easily craft a query to get the participants of a public
        # discussion and do a has_child filter with inner_hits on a private discussion.
        if 'inner_hits' in hit:
            del hit['inner_hits']

        if creator_id is not None:
            source['creator_name'] = creators_by_id.get(creator_id)

        if hit['_type'] == 'idea':
            idea = models.Idea.get_instance(source['id'])
            # The check is not really necessary because it's the same
            # 'read' permission as the discussion, but it doesn't cost anything
            # to check it and the READ permission may change in the future.
            if not idea.user_can(user_id, CrudPermissions.READ, permissions):
                raise HTTPUnauthorized

            source['num_posts'] = idea.num_posts
            source['num_contributors'] = idea.num_contributors
        elif hit['_type'] == 'user':
            agent_profile = models.AgentProfile.get_instance(source['id'])
            if not agent_profile.user_can(user_id, CrudPermissions.READ,
                                          permissions):
                raise HTTPUnauthorized

            source['num_posts'] = agent_profile.count_posts_in_discussion(
                discussion_id)
        # Don't do an extra request to verify the CrudPermissions.READ permission
        # for post or synthesis.
        # It's currently the same 'read' permission as the discussion.
        # elif hit['_type'] in ('synthesis', 'post'):
        #     post = models.Post.get_instance(source['id'])
        #     if not post.user_can(user_id, CrudPermissions.READ, permissions):
        #         raise HTTPUnauthorized

    return result
Example #58
0
    def mutate(root, args, context, info):
        cls = models.Thematic
        discussion_id = context.matchdict['discussion_id']
        user_id = context.authenticated_userid or Everyone

        thematic_id = args.get('id')
        id_ = int(Node.from_global_id(thematic_id)[1])
        thematic = cls.get(id_)

        permissions = get_permissions(user_id, discussion_id)
        allowed = thematic.user_can(user_id, CrudPermissions.UPDATE,
                                    permissions)
        if not allowed or (allowed == IF_OWNED and user_id == Everyone):
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush:
            title_entries = args.get('title_entries')
            if len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(thematic, 'title',
                                                 title_entries)
            update_langstring_from_input_entries(
                thematic, 'description', args.get('description_entries'))
            kwargs = {}
            video = args.get('video')
            if video is not None:
                update_langstring_from_input_entries(thematic, 'video_title',
                                                     video['title_entries'])
                update_langstring_from_input_entries(
                    thematic, 'video_description',
                    video['description_entries'])
                kwargs['video_html_code'] = video['html_code']

            # take the first entry and set it for short_title
            kwargs['short_title'] = title_entries[0]['value']
            kwargs['identifier'] = args.get('identifier')
            for attr, value in kwargs.items():
                setattr(thematic, attr, value)
            db = thematic.db
            db.flush()

            questions_input = args.get('questions')
            existing_questions = {
                question.id: question
                for question in thematic.get_children()
            }
            updated_questions = set()
            if questions_input is not None:
                for question_input in questions_input:
                    if question_input.get('id', None) is not None:
                        id_ = int(Node.from_global_id(question_input['id'])[1])
                        updated_questions.add(id_)
                        question = models.Question.get(id_)
                        update_langstring_from_input_entries(
                            question, 'title', question_input['title_entries'])
                    else:
                        title_ls = langstring_from_input_entries(
                            question_input['title_entries'])
                        models.Question(title=title_ls,
                                        discussion_id=discussion_id)
                        thematic.children.append(
                            models.Question(title=title_ls,
                                            discussion_id=discussion_id))

            # remove question (tombstone it) that are not in questions_input
            for question_id in set(
                    existing_questions.keys()).difference(updated_questions):
                existing_questions[
                    question_id].tombstone_date = datetime.utcnow()

            db.flush()

        return UpdateThematic(thematic=thematic)
Example #59
0
    def mutate(root, args, context, info):
        PROFILE_PICTURE = models.AttachmentPurpose.PROFILE_PICTURE.value
        cls = models.User
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        global_id = args.get('id')
        id_ = int(Node.from_global_id(global_id)[1])
        user = cls.get(id_)

        permissions = get_permissions(user_id, discussion_id)
        allowed = user.user_can(
            user_id, CrudPermissions.UPDATE, permissions)
        if not allowed:
            raise HTTPUnauthorized("You don't have the authorization to update this user. If you think it's an error, please reconnect to assembl.")

        with cls.default_db.no_autoflush as db:
            username = args.get('username')
            # only modify the username if it was given in parameter
            if username is not None:
                if username != user.username_p:
                    if db.query(models.Username).filter_by(
                        username=username
                    ).count():
                        raise Exception(u"001: We already have a user with this username.")

                user.username_p = username

            name = args.get('name')
            # only modify the name if it was given in parameter
            if name is not None:
                user.real_name_p = name

            old_password = args.get('old_password')
            new_password = args.get('new_password')
            new_password2 = args.get('new_password2')
            # only modify the password if it was given in parameter
            if old_password is not None and new_password is not None and new_password2 is not None:
                if not user.check_password(old_password):
                    raise Exception(u"002: The entered password doesn't match your current password.")

                if new_password != new_password2:
                    raise Exception(u"003: You entered two different passwords.")

                if old_password == new_password:
                    raise Exception(u"004: The new password has to be different than the current password.")

                try:
                    user.password_p = new_password
                except LocalizableError as e:
                    raise JSONError(e.localized_message(context.localizer))
                except Exception as e:
                    raise JSONError(str(e), code=409)

            # add uploaded image as an attachment to the user
            image = args.get('image')
            if image is not None:
                filename = os.path.basename(context.POST[image].filename)
                mime_type = context.POST[image].type
                document = models.File(
                    discussion=discussion,
                    mime_type=mime_type,
                    title=filename)
                document.add_file_data(context.POST[image].file)
                # if there is already an PROFILE_PICTURE, remove it with the
                # associated document
                images = [
                    att for att in user.profile_attachments
                    if att.attachmentPurpose == PROFILE_PICTURE]
                if images:
                    image = images[0]
                    allowed = image.user_can(
                        user_id, CrudPermissions.DELETE, permissions)
                    if not allowed:
                        raise HTTPUnauthorized("The authenticated user can't delete the existing AgentProfileAttachment")

                    image.document.delete_file()
                    db.delete(image.document)
                    user.profile_attachments.remove(image)

                allowed = models.AgentProfileAttachment.user_can_cls(
                    user_id, CrudPermissions.CREATE, permissions)
                if not allowed:
                    raise HTTPUnauthorized("The authenticated user can't create an AgentProfileAttachment")

                discussion.db.add(models.AgentProfileAttachment(
                    document=document,
                    discussion=discussion,
                    user=user,
                    creator_id=context.authenticated_userid,
                    title=filename,
                    attachmentPurpose=PROFILE_PICTURE
                ))

            db.flush()

        return UpdateUser(user=user)
Example #60
0
    def mutate(root, args, context, info):
        EMBED_ATTACHMENT = models.AttachmentPurpose.EMBED_ATTACHMENT.value
        MEDIA_ATTACHMENT = models.AttachmentPurpose.MEDIA_ATTACHMENT.value
        cls = models.Idea
        discussion_id = context.matchdict['discussion_id']
        discussion = models.Discussion.get(discussion_id)
        user_id = context.authenticated_userid or Everyone

        thematic_id = args.get('id')
        id_ = int(Node.from_global_id(thematic_id)[1])
        thematic = cls.get(id_)

        permissions = get_permissions(user_id, discussion_id)
        allowed = thematic.user_can(user_id, CrudPermissions.UPDATE,
                                    permissions)
        if not allowed:
            raise HTTPUnauthorized()

        with cls.default_db.no_autoflush as db:
            # introducing history at every step, including thematics + questions  # noqa: E501
            thematic.copy(tombstone=True)
            title_entries = args.get('title_entries')
            if title_entries is not None and len(title_entries) == 0:
                raise Exception(
                    'Thematic titleEntries needs at least one entry')
                # Better to have this message than
                # 'NoneType' object has no attribute 'owner_object'
                # when creating the saobj below if title=None

            update_langstring_from_input_entries(thematic, 'title',
                                                 title_entries)
            update_langstring_from_input_entries(
                thematic, 'description', args.get('description_entries'))
            kwargs = {}
            video = args.get('video', None)
            if video is not None:
                update_langstring_from_input_entries(
                    thematic, 'video_title', video.get('title_entries', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_top',
                    video.get('description_entries_top', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_bottom',
                    video.get('description_entries_bottom', []))
                update_langstring_from_input_entries(
                    thematic, 'video_description_side',
                    video.get('description_entries_side', []))
                kwargs['video_html_code'] = video.get('html_code', None)

                video_media = video.get('media_file', None)
                if video_media:
                    update_attachment(discussion, models.IdeaAttachment,
                                      video_media, thematic.attachments,
                                      MEDIA_ATTACHMENT, db, context)

            kwargs['message_view_override'] = args.get('message_view_override')

            for attr, value in kwargs.items():
                setattr(thematic, attr, value)

            # change order if needed
            order = args.get('order')
            if order:
                thematic.source_links[0].order = order

            # add uploaded image as an attachment to the idea
            image = args.get('image')
            if image is not None:
                update_attachment(discussion, models.IdeaAttachment, image,
                                  thematic.attachments, EMBED_ATTACHMENT, db,
                                  context)

            # Create the idea announcement object which corresponds to the instructions
            announcement = args.get('announcement')
            if announcement is not None:
                announcement_title_entries = announcement.get('title_entries')
                if len(announcement_title_entries) == 0:
                    raise Exception(
                        'Announcement titleEntries needs at least one entry')

                announcement_title_langstring = langstring_from_input_entries(
                    announcement_title_entries)
                announcement_body_langstring = langstring_from_input_entries(
                    announcement.get('body_entries', None))
                saobj2 = create_idea_announcement(
                    user_id, discussion, thematic,
                    announcement_title_langstring,
                    announcement_body_langstring)
                db.add(saobj2)

            questions_input = args.get('questions')
            existing_questions = {
                question.id: question
                for question in thematic.get_children()
            }
            updated_questions = set()
            if questions_input is not None:
                for idx, question_input in enumerate(questions_input):
                    if question_input.get('id', None) is not None:
                        id_ = int(Node.from_global_id(question_input['id'])[1])
                        updated_questions.add(id_)
                        question = models.Question.get(id_)
                        # archive the question
                        question.copy(tombstone=True)
                        update_langstring_from_input_entries(
                            question, 'title', question_input['title_entries'])
                        # modify question order
                        question.source_links[0].order = idx + 1.0
                    else:
                        title_ls = langstring_from_input_entries(
                            question_input['title_entries'])
                        question = models.Question(title=title_ls,
                                                   discussion_id=discussion_id)
                        db.add(
                            models.IdeaLink(source=thematic,
                                            target=question,
                                            order=idx + 1.0))

                # remove question (tombstone) that are not in questions_input
                for question_id in set(existing_questions.keys()).difference(
                        updated_questions):
                    existing_questions[question_id].is_tombstone = True

            db.flush()

        return UpdateThematic(thematic=thematic)