Exemplo n.º 1
0
def switch_control_lists_access(
    corpus: Corpus,
    users: List[User],
    old_control_lists_id: int,
):
    """Switch user access from one control list to another.

    :param list users: list of users to switch
    :param int old_control_lists_id: ID of old control list
    """
    for user in users:
        # do not delete access to old control list if another corpus uses it
        for user_corpus in Corpus.for_user(user):
            if user_corpus.control_lists_id == old_control_lists_id and user_corpus.id != corpus.id:
                break
        else:
            control_lists_user = ControlListsUser.query.filter(
                ControlListsUser.control_lists_id == old_control_lists_id,
                ControlListsUser.user_id == user.id,
            ).one_or_none()
            # do not delete access to old control list if user is owner
            if control_lists_user and not control_lists_user.is_owner:
                db.session.delete(control_lists_user)
        # add access to new control list
        ControlLists.link(corpus, user)
    db.session.commit()
Exemplo n.º 2
0
    def add_control_lists(self):
        """ Loads a control list from a folder as a public one

        :param folder_name: Name of the folder in app/configurations/langs
        """
        ControlLists.add_default_lists()
        db.session.commit()
Exemplo n.º 3
0
    def db_create():
        """ Creates a local database
        """
        with app.app_context():
            db.create_all()

            Role.add_default_roles()
            User.add_default_users()
            ControlLists.add_default_lists()

            db.session.commit()
            click.echo("Created the database")
Exemplo n.º 4
0
    def db_recreate():
        """ Recreates a local database. You probably should not use this on
        production.
        """
        with app.app_context():
            db.drop_all()
            db.create_all()

            Role.add_default_roles()
            User.add_default_users()
            ControlLists.add_default_lists()

            db.session.commit()
            click.echo("Dropped then recreated the database")
Exemplo n.º 5
0
def rename(control_list_id):
    """ This routes allows user to send email to list administrators
    """
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    form = Rename(prefix="rename")
    control_list_link = url_for('control_lists_bp.get',
                                control_list_id=control_list_id,
                                _external=True)

    if not is_owner and not current_user.is_admin():
        flash("You are not an owner of the list.", category="error")
        return redirect(control_list_link)

    if request.method == "POST" and form.validate_on_submit():
        control_list.name = form.title.data
        db.session.add(control_list)
        try:
            db.session.commit()
            flash("The name of the list has been updated.", category="success")
        except:
            flash(
                "There was an error when we tried to rename your control list.",
                category="error")
        return redirect(control_list_link)
    return render_template_with_nav_info('control_lists/rename.html',
                                         form=form,
                                         control_list=control_list)
Exemplo n.º 6
0
def contact(control_list_id):
    """ This routes allows user to send email to list administrators
    """
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)

    form = SendMailToAdmin(prefix="mail")

    if request.method == "POST" and form.validate_on_submit():
        control_list_link = url_for('control_lists_bp.get',
                                    control_list_id=control_list_id,
                                    _external=True)
        email.send_email_async(
            app=current_app._get_current_object(),
            bcc=[u[3] for u in control_list.owners] + [current_user.email],
            recipient=[],
            subject='[Pyrrha Control List] ' + form.title.data,
            template='control_lists/email/contact',
            # current_user is a LocalProxy, we want the underlying user
            # object
            user=current_user._get_current_object(),
            message=form.message.data,
            control_list_title=control_list.name,
            url=control_list_link)
        flash('The email has been sent to the control list administrators.',
              'success')
        return redirect(
            url_for('control_lists_bp.contact',
                    control_list_id=control_list_id))
    return render_template_with_nav_info('control_lists/contact.html',
                                         form=form,
                                         control_list=control_list)
Exemplo n.º 7
0
def get(control_list_id):
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    return render_template_with_nav_info(
        "control_lists/control_list.html",
        control_list=control_list,
        is_owner=is_owner,
        can_edit=is_owner or current_user.is_admin(),
    )
Exemplo n.º 8
0
    def normal_view():
        lists = {"public": [], "submitted": [], "private": []}
        for cl in ControlLists.get_available(current_user):
            lists[cl.str_public].append(cl)

        return render_template_with_nav_info('main/corpus_new.html',
                                             lemmatizers=lemmatizers,
                                             public_control_lists=lists,
                                             tsv=request.form.get("tsv", ""))
Exemplo n.º 9
0
def edit(cl_id, allowed_type):
    """ Find allowed values and allow their edition

    :param cl_id: Id of the Control List
    :param allowed_type: Type of allowed value (lemma, morph, POS)
    """
    if allowed_type not in ["lemma", "POS", "morph"]:
        raise NotFound("Unknown type of resource.")
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=cl_id, user=current_user)

    can_edit = is_owner or current_user.is_admin()

    if not can_edit:
        return abort(403)

    # In case of Post
    if request.method == "POST":
        allowed_values = request.form.get("allowed_values")
        if allowed_type == "lemma":
            allowed_values = [
                x.replace('\r', '') for x in allowed_values.split("\n")
                if len(x.replace('\r', '').strip()) > 0
            ]
        elif allowed_type == "POS":
            allowed_values = [
                x.replace('\r', '') for x in allowed_values.split(",")
                if len(x.replace('\r', '').strip()) > 0
            ]
        else:
            allowed_values = list(StringDictReader(allowed_values))
        success = control_list.update_allowed_values(allowed_type,
                                                     allowed_values)
        if success:
            flash("Control List Updated", category="success")
        else:
            flash("An error occured", category="error")

    values = control_list.get_allowed_values(allowed_type=allowed_type,
                                             order_by="id")
    if allowed_type == "lemma":
        format_message = "This should be formatted as a list of lemma separated by new line"
        values = "\n".join([d.label for d in values])
    elif allowed_type == "POS":
        format_message = "This should be formatted as a list of POS separated by comma and no space"
        values = ",".join([d.label for d in values])
    else:
        format_message = "The TSV should at least have the header : label and could have a readable column for human"
        values = "\n".join(
            ["label\treadable"] +
            ["{}\t{}".format(d.label, d.readable) for d in values])
    return render_template_with_nav_info("control_lists/edit.html",
                                         format_message=format_message,
                                         values=values,
                                         allowed_type=allowed_type,
                                         control_list=control_list)
Exemplo n.º 10
0
def _get_available():
    """Prepare dictionary of available control lists.

    :returns: available control lists
    :rtype: dict
    """
    lists = {"public": [], "submitted": [], "private": []}
    for cl in ControlLists.get_available(current_user):
        lists[cl.str_public].append(cl)
    return lists
Exemplo n.º 11
0
 def decorated_view(*args, **kwargs):
     control_list, is_owner = ControlLists.get_linked_or_404(
         control_list_id=kwargs[control_list_param], user=current_user)
     can_edit = is_owner or current_user.is_admin()
     if not can_edit:
         flash("You are not an owner of the list.", category="error")
         return redirect(
             url_for(".get",
                     control_list_id=kwargs[control_list_param]))
     return func(*args, control_list=control_list, **kwargs)
Exemplo n.º 12
0
def dashboard():
    """admin dashboard page."""
    if current_user.is_admin():
        corpora = db.session.query(Corpus).all()
        control_lists = db.session.query(ControlLists).all()
    else:
        corpora = Corpus.for_user(current_user)
        control_lists = ControlLists.for_user(current_user)
    return render_template_with_nav_info('main/dashboard.html',
                                         current_user=current_user,
                                         dashboard_corpora=corpora,
                                         dashboard_control_lists=control_lists)
Exemplo n.º 13
0
    def test_db_create(self):
        """ Test that db is created """

        result = self.invoke(["db-create"])
        self.assertIn("Created the database", result.output)
        with self.app.app_context():
            cl = ControlLists(name="Corpus1")
            db.session.add(cl)
            db.session.flush()
            db.session.add(Corpus(name="Corpus1", control_lists_id=cl.id))
            db.session.commit()

            self.assertEqual(len(Corpus.query.all()), 1,
                             "There should have been an insert")
Exemplo n.º 14
0
def propose_as_public(control_list_id):
    """ This routes allows user to send email to application administrators
    to propose a list as public for everyone to use

    """
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)

    if not is_owner:
        flash("You are not an owner of the list.", category="error")
        return redirect(
            url_for("control_lists_bp.get", control_list_id=control_list_id))
    elif control_list.public != PublicationStatus.private:
        flash("This list is already public or submitted.", category="warning")
        return redirect(
            url_for("control_lists_bp.get", control_list_id=control_list_id))

    form = SendMailToAdmin(prefix="mail")

    if form.validate_on_submit():
        admins = User.get_admins()
        control_list_link = url_for('control_lists_bp.get',
                                    control_list_id=control_list_id,
                                    _external=True)
        control_list.public = PublicationStatus.submitted
        db.session.add(control_list)
        try:
            email.send_email_async(
                app=current_app._get_current_object(),
                bcc=[u.email for u in admins] + [current_user.email],
                recipient=[],
                subject='[Pyrrha Control List] ' + form.title.data,
                template='control_lists/email/contact',
                # current_user is a LocalProxy, we want the underlying user
                # object
                user=current_user._get_current_object(),
                message=form.message.data,
                control_list_title=control_list.name,
                url=control_list_link)
            flash('The email has been sent to the administrators.', 'success')
            db.session.commit()
        except Exception:
            db.session.rollback()
            flash("There was an error during the messaging step")
    return render_template_with_nav_info(
        'control_lists/propose_as_public.html',
        form=form,
        control_list=control_list)
Exemplo n.º 15
0
def go_public(control_list_id):
    """ This routes makes a list public

    """
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    if not current_user.is_admin():
        flash("You do not have the rights for this action.", category="error")
    elif control_list.public == PublicationStatus.public:
        flash("This list is already public.", category="warning")
    else:
        control_list.public = PublicationStatus.public
        db.session.add(control_list)
        try:
            db.session.commit()
            flash('This list is now public.', 'success')
        except Exception:
            db.session.rollback()
            flash("There was an error during the update.", category="error")

    return redirect(
        url_for("control_lists_bp.get", control_list_id=control_list_id))
Exemplo n.º 16
0
def read_allowed_values(control_list_id, allowed_type):
    if allowed_type not in ["POS", "morph"]:
        flash("The category you selected is wrong petit coquin !",
              category="error")
        return redirect(url_for(".get", control_list_id=control_list_id))

    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    kwargs = {}

    template = "control_lists/read.html"
    allowed_values = control_list.get_allowed_values(
        allowed_type=allowed_type).all()

    return render_template_with_nav_info(template=template,
                                         control_list=control_list,
                                         is_owner=is_owner,
                                         can_edit=is_owner
                                         or current_user.is_admin(),
                                         allowed_type=allowed_type,
                                         allowed_values=allowed_values,
                                         readable=allowed_type == "morph",
                                         **kwargs)
Exemplo n.º 17
0
def information_read(control_list_id):
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    return render_template_with_nav_info('control_lists/information_read.html',
                                         control_list=control_list)
Exemplo n.º 18
0
from app.models import Corpus, WordToken, Column
from app.models import ControlLists

control_list = ControlLists(id=3, name="Latin")
corpus = Corpus(
    name="Priapees",
    id=3,
    control_lists_id=control_list.id,
)
PriapeeColumns = [
    Column(heading="Lemma", corpus_id=3),
    Column(heading="POS", corpus_id=3),
    Column(heading="Morph", corpus_id=3),
    Column(heading="Similar", corpus_id=3),
]
tokens = [
    WordToken(corpus=corpus.id, form="Carminis", lemma="carmen1", POS="NOMcom", left_context="Carminis incompti lusus lecture", right_context="procaces ,", label_uniform="carmen1", morph="Case=Gen|Numb=Sing"),
    WordToken(corpus=corpus.id, form="incompti", lemma="incomptus", POS="ADJqua", left_context="Carminis incompti lusus lecture", right_context="procaces , conueniens", label_uniform="incomptus", morph="Case=Gen|Numb=Sing|Deg=Pos"),
    WordToken(corpus=corpus.id, form="lusus", lemma="lusus", POS="NOMcom", left_context="Carminis incompti lusus lecture", right_context="procaces , conueniens Latio", label_uniform="lusus", morph="Case=Gen|Numb=Sing"),
    WordToken(corpus=corpus.id, form="lecture", lemma="lego?", POS="VER", left_context="Carminis incompti lusus lecture", right_context="procaces , conueniens Latio pone", label_uniform="lego?", morph="Case=Voc|Numb=Sing|Mood=Par|Voice=Act"),
    WordToken(corpus=corpus.id, form="procaces", lemma="procax", POS="ADJqua", left_context="Carminis incompti lusus lecture", right_context="procaces , conueniens Latio pone supercilium", label_uniform="procax", morph="Case=Acc|Numb=Plur|Deg=Pos"),
    WordToken(corpus=corpus.id, form=",", lemma=",", POS="PUNC", left_context="Carminis incompti lusus lecture", right_context="procaces , conueniens Latio pone supercilium .", label_uniform=",", morph="MORPH=empty"),
    WordToken(corpus=corpus.id, form="conueniens", lemma="conueniens", POS="ADJqua", left_context="incompti lusus lecture procaces", right_context=", conueniens Latio pone supercilium . non", label_uniform="conueniens", morph="Case=Nom|Numb=Sing|Deg=Pos"),
    WordToken(corpus=corpus.id, form="Latio", lemma="latio", POS="NOMcom", left_context="lusus lecture procaces ,", right_context="conueniens Latio pone supercilium . non soror", label_uniform="latio", morph="Case=Nom|Numb=Sing"),
    WordToken(corpus=corpus.id, form="pone", lemma="pono", POS="VER", left_context="lecture procaces , conueniens", right_context="Latio pone supercilium . non soror hoc", label_uniform="pono", morph="Numb=Sing|Mood=Imp|Tense=Pres|Voice=Act|Person=2"),
    WordToken(corpus=corpus.id, form="supercilium", lemma="supercilium", POS="NOMcom", left_context="procaces , conueniens Latio", right_context="pone supercilium . non soror hoc habitat", label_uniform="supercilium", morph="Case=Acc|Numb=Sing"),
    WordToken(corpus=corpus.id, form=".", lemma=".", POS="PUNC", left_context=", conueniens Latio pone", right_context="supercilium . non soror hoc habitat Phoebi", label_uniform=".", morph="MORPH=empty"),
    WordToken(corpus=corpus.id, form="non", lemma="non", POS="ADVneg", left_context="conueniens Latio pone supercilium", right_context=". non soror hoc habitat Phoebi ,", label_uniform="non", morph="MORPH=empty"),
    WordToken(corpus=corpus.id, form="soror", lemma="soror", POS="NOMcom", left_context="Latio pone supercilium .", right_context="non soror hoc habitat Phoebi , non", label_uniform="soror", morph="Case=Nom|Numb=Sing"),
    WordToken(corpus=corpus.id, form="hoc", lemma="hic1", POS="PROdem", left_context="pone supercilium . non", right_context="soror hoc habitat Phoebi , non uesta", label_uniform="hic1", morph="Case=Nom|Numb=Sing"),
    WordToken(corpus=corpus.id, form="habitat", lemma="habito", POS="VER", left_context="supercilium . non soror", right_context="hoc habitat Phoebi , non uesta sacello", label_uniform="habito", morph="Numb=Sing|Mood=Ind|Tense=Pres|Voice=Act|Person=3"),
Exemplo n.º 19
0
from app.models import Corpus, WordToken, AllowedLemma, AllowedPOS, AllowedMorph, Column
from app.models import ControlLists, ControlListsUser


Floovant = Corpus(
    name="Floovant",
    id=2,
    control_lists_id=2
)
FloovantColumns = [
    Column(heading="Lemma", corpus_id=2),
    Column(heading="POS", corpus_id=2),
    Column(heading="Morph", corpus_id=2),
    Column(heading="Similar", corpus_id=2),
]
FCL = ControlLists(id=2, name="Floovant")
FloovantTokens = [
    WordToken(corpus=Floovant.id, form="SOIGNORS", lemma="seignor", left_context="", right_context="or escoutez que",
              label_uniform="seignor", morph="NOMB.=p|GENRE=m|CAS=n"),
    WordToken(corpus=Floovant.id, form="or", lemma="or4", left_context="SOIGNORS", right_context="escoutez que Dés",
              label_uniform="or4", morph="DEGRE=-"),
    WordToken(corpus=Floovant.id, form="escoutez", lemma="escouter", left_context="SOIGNORS or",
              right_context="que Dés vos", label_uniform="escouter", morph="MODE=imp|PERS.=2|NOMB.=p"),
    WordToken(corpus=Floovant.id, form="que", lemma="que4", left_context="SOIGNORS or escoutez",
              right_context="Dés vos soit", label_uniform="que4", morph="_"),
    WordToken(corpus=Floovant.id, form="Dés", lemma="dieu", left_context="or escoutez que",
              right_context="vos soit amis", label_uniform="dieu", morph="NOMB.=s|GENRE=m|CAS=n"),
    WordToken(corpus=Floovant.id, form="vos", lemma="vos1", left_context="escoutez que Dés",
              right_context="soit amis III", label_uniform="vos1", morph="PERS.=2|NOMB.=p|GENRE=m|CAS=r"),
    WordToken(corpus=Floovant.id, form="soit", lemma="estre1", left_context="que Dés vos",
              right_context="amis III vers", label_uniform="estre1", morph="MODE=sub|TEMPS=pst|PERS.=3|NOMB.=s"),
Exemplo n.º 20
0
def lemma_list(control_list_id):
    control_list, is_owner = ControlLists.get_linked_or_404(
        control_list_id=control_list_id, user=current_user)
    can_edit = is_owner or current_user.is_admin()
    if request.method == "DELETE" and can_edit:
        value = request.args.get("id")
        lemma = AllowedLemma.query.get_or_404(value)
        try:
            AllowedLemma.query.filter(
                AllowedLemma.id == lemma.id,
                AllowedLemma.control_list == control_list_id).delete()
            db.session.commit()
            return "", 200
        except Exception as E:
            db.session.rollback()
            return abort(403)
    elif request.method == "UPDATE" and request.mimetype == "application/json" and can_edit:
        form = request.get_json().get("lemmas", None)
        if not form:
            return abort(400, jsonify({"message": "No lemma were passed."}))
        lemmas = list(set(form.split()))
        try:
            AllowedLemma.add_batch(lemmas, control_list.id, _commit=True)
            return jsonify({"message": "Data saved"})
        except ValueError as E:
            db.session.rollback()
            return make_response(jsonify({"message": str(E)}), 400)
        except sqlalchemy.exc.StatementError as E:
            db.session.rollback()
            error = str(E.orig)
            if error.startswith("UNIQUE constraint failed"):
                return make_response(
                    jsonify({
                        "message":
                        "One of the lemma you submitted already exist. "
                        "Remove this lemma and resubmit."
                    }), 400)
            return make_response(
                jsonify(
                    {"message": "Database error. Contact the administrator."}),
                400)
        except Exception as E:
            db.session.rollback()
            return make_response(jsonify({"message": "Unknown Error"}), 400)
    elif request.method == "GET":
        kwargs = {}
        page = request.args.get("page", "1")
        page = (page.isnumeric()) and int(page) or 1

        limit = request.args.get("limit", "1000")
        limit = (limit.isnumeric()) and int(limit) or 1
        kw = strip_or_none(request.args.get("kw", ""))
        template = "control_lists/read_lemma.html"
        allowed_values = control_list.get_allowed_values(allowed_type="lemma",
                                                         kw=kw).paginate(
                                                             page=page,
                                                             per_page=limit)
        kwargs["kw"] = kw

        return render_template_with_nav_info(template=template,
                                             control_list=control_list,
                                             is_owner=is_owner,
                                             allowed_type="lemma",
                                             can_edit=is_owner
                                             or current_user.is_admin(),
                                             allowed_values=allowed_values,
                                             readable=False,
                                             **kwargs)
    return abort(405)
Exemplo n.º 21
0
def corpus_new():
    """ Register a new corpus
    """
    lemmatizers = current_app.config.get("LEMMATIZERS", [])

    def normal_view():
        return render_template_with_nav_info(
            'main/corpus_new.html',
            lemmatizers=lemmatizers,
            public_control_lists=_get_available(),
            tsv=request.form.get("tsv", "")
        )

    def error():
        return normal_view(), 400

    if request.method == "POST":
        if not current_user.is_authenticated:
            abort(403)
        elif not len(strip_or_none(request.form.get("name", ""))):
            flash("You forgot to give a name to your corpus", category="error")
            return error()
        else:
            form_kwargs = {
                "name": request.form.get("name"),
                "context_left": request.form.get("context_left", None),
                "context_right": request.form.get("context_right", None),
                "delimiter_token": strip_or_none(request.form.get("sep_token", "")) or None,
                "columns": [
                    Column(heading="Lemma"),
                    Column(heading="POS"),
                    Column(heading="Morph"),
                    Column(heading="Similar"),
                ]
            }
            for column in form_kwargs["columns"]:
                column.hidden = bool(
                    request.form.get(f"{column.heading.lower()}Column", "")
                )
            if (
                "lemmaColumn" in request.form
                and "posColumn" in request.form
                and "morphColumn" in request.form
            ):
                flash(
                    "You can't disable Lemma and POS and Morph. Keep at least one of them.",
                    category="error"
                )
                return error()

            if request.form.get("control_list") == "reuse":
                tokens = read_input_tokens(request.form.get("tsv"))
                try:
                    control_list = ControlLists.query.get_or_404(request.form.get("control_list_select"))
                except Exception:
                    flash("This control list does not exist", category="error")
                    return error()
                form_kwargs.update({"word_tokens_dict": tokens,
                                    "control_list": control_list})
                cl_owner = False
            else:
                tokens, allowed_lemma, allowed_morph, allowed_POS = create_input_format_convertion(
                    request.form.get("tsv"),
                    request.form.get("allowed_lemma", None),
                    request.form.get("allowed_morph", None),
                    request.form.get("allowed_POS", None)
                )
                cl_owner = True
                form_kwargs.update({"word_tokens_dict": tokens, "allowed_lemma": allowed_lemma,
                                    "allowed_POS": allowed_POS, "allowed_morph": allowed_morph})

            try:
                corpus = Corpus.create(**form_kwargs)
                db.session.add(CorpusUser(corpus=corpus, user=current_user, is_owner=True))
                # Add a link to the control list
                ControlLists.link(corpus=corpus, user=current_user, is_owner=cl_owner)
                db.session.commit()
                flash("New corpus registered", category="success")
                return redirect(url_for(".corpus_get", corpus_id=corpus.id))
            except (sqlalchemy.exc.StatementError, sqlalchemy.exc.IntegrityError) as e:
                db.session.rollback()
                flash("The corpus cannot be registered. Check your data", category="error")
                if str(e.orig) == "UNIQUE constraint failed: corpus.name":
                    flash("You have already a corpus going by the name {}".format(request.form.get("name")),
                          category="error")
                return error()
            except MissingTokenColumnValue as exc:
                db.session.rollback()
                flash("At least one line of your corpus is missing a token/form. Check line %s " % exc.line, category="error")
                return error()
            except NoTokensInput:
                db.session.rollback()
                flash("You did not input any text.", category="error")
                return error()
            except ValidationError as exception:
                db.session.rollback()
                flash(exception, category="error")
                return error()
            except Exception as e:
                db.session.rollback()
                flash("The corpus cannot be registered. Check your data", category="error")
                return error()

    return normal_view()
Exemplo n.º 22
0
from app.models import ChangeRecord, WordToken, Corpus, ControlLists
from .base import TestModels
import copy

SimilarityFixtures = [
    ControlLists(id=1, name="CL Fixture"),
    Corpus(id=1, name="Fixtures !", control_lists_id=1),
    WordToken(corpus=1,
              form="Cil",
              lemma="celui",
              left_context="_",
              right_context="_",
              label_uniform="celui",
              morph="smn",
              POS="p"),  # 1
    WordToken(corpus=1,
              form="Cil",
              lemma="celle",
              left_context="_",
              right_context="_",
              label_uniform="celle",
              morph="smn",
              POS="n"),  # 2
    WordToken(corpus=1,
              form="Cil",
              lemma="cil",
              left_context="_",
              right_context="_",
              label_uniform="cil",
              morph="smn",
              POS="p"),  # 3