From 516d262b14584202c4a570ce3a8c33e14b27f564 Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Mon, 8 Apr 2019 13:53:22 +0530
Subject: [PATCH 001/125] Revised labelset and label models
---
funnel/models/__init__.py | 1 +
funnel/models/label.py | 76 +++++++++++++++++++++------------------
funnel/models/project.py | 4 ++-
3 files changed, 46 insertions(+), 35 deletions(-)
diff --git a/funnel/models/__init__.py b/funnel/models/__init__.py
index f05ffa91a..1c1a3bd65 100644
--- a/funnel/models/__init__.py
+++ b/funnel/models/__init__.py
@@ -19,3 +19,4 @@
from .session import *
from .user import *
from .venue import *
+from .label import *
diff --git a/funnel/models/label.py b/funnel/models/label.py
index f20ff2cdb..cf2f884f4 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -1,67 +1,75 @@
# -*- coding: utf-8 -*-
-from . import db, make_timestamp_columns, TimestampMixin, BaseScopedNameMixin
-from .profile import Profile
+from sqlalchemy.ext.orderinglist import ordering_list
+
+from . import db, BaseScopedNameMixin
from .project import Project
from .proposal import Proposal
class Labelset(BaseScopedNameMixin, db.Model):
"""
- A collection of labels, in checkbox mode (select multiple) or radio mode (select one). A profile can
- contain multiple label sets and a Project can enable one or more sets to be used in Proposals.
+ A collection of labels, in checkbox mode (select multiple) or radio mode (select one). A project can
+ contain multiple label sets.
"""
__tablename__ = 'labelset'
- profile_id = db.Column(None, db.ForeignKey('profile.id'), nullable=False)
- profile = db.relationship(Profile, backref=db.backref('labelsets', cascade='all, delete-orphan'))
- parent = db.synonym('profile')
+ project_id = db.Column(None, db.ForeignKey('profile.id', ondelete='CASCADE'), nullable=False)
+ project = db.relationship(Project) # Backref is defined in the Project model with an ordering list
+ parent = db.synonym('project')
+
+ labels = db.relationship('Label', cascade='all, delete-orphan',
+ order_by='Label.seq', collection_class=ordering_list('seq', count_from=1))
+
+ #: Sequence number for this labelset, used in UI for ordering
+ seq = db.Column(db.Integer, nullable=False)
+ #: Radio mode specifies that only one of the labels in this set may be applied on a project
radio_mode = db.Column(db.Boolean, nullable=False, default=False)
+ #: Restricted model specifies that labels in this set may only be applied by someone with
+ #: an editorial role (TODO: name the role)
+ restricted = db.Column(db.Boolean, nullable=False, default=False)
- __table_args__ = (db.UniqueConstraint('profile_id', 'name'),)
+ __table_args__ = (db.UniqueConstraint('project_id', 'name'),)
def __repr__(self):
- return "" % (self.name, self.profile.name)
+ return "" % (self.name, self.project.name)
proposal_label = db.Table(
'proposal_label', db.Model.metadata,
- *(make_timestamp_columns() + (
- db.Column('proposal_id', None, db.ForeignKey('proposal.id'), nullable=False, primary_key=True),
- db.Column('label_id', None, db.ForeignKey('label.id'), nullable=False, primary_key=True)
- )))
+ db.Column('proposal_id', None, db.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False, primary_key=True),
+ db.Column('label_id', None, db.ForeignKey('label.id', ondelete='CASCADE'), nullable=False, primary_key=True, index=True),
+ db.Column('created_at', db.DateTime, default=db.func.utcnow())
+)
class Label(BaseScopedNameMixin, db.Model):
__tablename__ = 'label'
- labelset_id = db.Column(None, db.ForeignKey('labelset.id'), nullable=False)
+ labelset_id = db.Column(None, db.ForeignKey('labelset.id', ondelete='CASCADE'), nullable=False)
labelset = db.relationship(Labelset)
- proposals = db.relationship(Proposal, secondary=proposal_label, backref='labels')
+ #: Sequence number for this label, used in UI for ordering
+ seq = db.Column(db.Integer, nullable=False)
+
+ #: Icon for displaying in space-constrained UI. Contains emoji
+ #: an emoji, or up to three ASCII characters picked from the label's title
+ icon_emoji = db.Column(db.Unicode(1), nullable=True)
+
+ #: Proposals that this label is attached to
+ proposals = db.relationship(Proposal, secondary=proposal_label, lazy='dynamic', backref='labels')
__table_args__ = (db.UniqueConstraint('labelset_id', 'name'),)
def __repr__(self):
return "
-
-
{% endif %}
From e99e34b436972c8cf710ce666e30844c56303c81 Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Mon, 15 Apr 2019 19:18:23 +0530
Subject: [PATCH 026/125] Remove LabelSet
Labels can refer to other labels as parents, which then effectively behave like sets.
---
funnel/models/label.py | 160 +++++++++++++++++++--------------------
funnel/models/project.py | 6 +-
2 files changed, 80 insertions(+), 86 deletions(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 8c72ac407..64cc5a98a 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -1,130 +1,114 @@
# -*- coding: utf-8 -*-
-from sqlalchemy import func
+from sqlalchemy.sql import exists
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.hybrid import hybrid_property
-from coaster.sqlalchemy import with_roles
-
from . import db, BaseScopedNameMixin
from .project import Project
from .proposal import Proposal
-class Labelset(BaseScopedNameMixin, db.Model):
- """
- A collection of labels, in checkbox mode (select multiple) or radio mode (select one). A project can
- contain multiple labelsets.
- """
- __tablename__ = 'labelset'
+proposal_label = db.Table(
+ 'proposal_label', db.Model.metadata,
+ db.Column('proposal_id', None, db.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False, primary_key=True),
+ db.Column('label_id', None, db.ForeignKey('label.id', ondelete='CASCADE'), nullable=False, primary_key=True, index=True),
+ db.Column('created_at', db.DateTime, default=db.func.utcnow())
+)
+
+
+class Label(BaseScopedNameMixin, db.Model):
+ __tablename__ = 'label'
project_id = db.Column(None, db.ForeignKey('project.id', ondelete='CASCADE'), nullable=False)
project = db.relationship(Project) # Backref is defined in the Project model with an ordering list
+ # `parent` is required for :meth:`~coaster.sqlalchemy.mixins.BaseScopedNameMixin.make_name()`
parent = db.synonym('project')
- labels = db.relationship('Label', cascade='all, delete-orphan',
- order_by='Label.seq', collection_class=ordering_list('seq', count_from=1))
+ _parent_label_id = db.Column(
+ 'parent_label_id',
+ None,
+ db.ForeignKey('label.id', ondelete='CASCADE', use_alter=True, name='label_parent_label_id_fkey'),
+ index=True,
+ nullable=True
+ )
+ # See https://docs.sqlalchemy.org/en/13/orm/self_referential.html
+ children = db.relationship(
+ 'Label',
+ backref=db.backref('parent_label', remote_side='Label.id'),
+ order_by='Label.seq',
+ collection_class=ordering_list('seq', count_from=1)
+ )
+
+ # TODO: Add sqlalchemy validator for `parent_label` to ensure the parent's project matches.
+ # Ideally add a SQL post-update trigger as well (code is in coaster's add_primary_relationship)
- #: Sequence number for this labelset, used in UI for ordering
+ #: Sequence number for this label, used in UI for ordering
seq = db.Column(db.Integer, nullable=False)
- # A single-line description of this labelset, shown when applying labels
+ # A single-line description of this label, shown when picking labels (optional)
description = db.Column(db.UnicodeText, nullable=False)
- #: Radio mode specifies that only one of the labels in this set may be applied on a project
- radio_mode = db.Column(db.Boolean, nullable=False, default=False)
+ #: Icon for displaying in space-constrained UI. Contains one emoji symbol.
+ #: Since emoji can be composed from multiple symbols, there is no length
+ #: limit imposed here
+ icon_emoji = db.Column(db.UnicodeText, nullable=True)
#: Restricted mode specifies that labels in this set may only be applied by someone with
#: an editorial role (TODO: name the role)
- restricted = db.Column(db.Boolean, nullable=False, default=False)
+ _restricted = db.Column('restricted', db.Boolean, nullable=False, default=False)
- #: Required mode specifies that a label from this set must be applied on a proposal.
- #: This is not foolproof and can be violated under a variety of conditions including
- #: (a) this flag being set after a proposal is created, and (b) a proposal being moved
- #: across projects
- required = db.Column(db.Boolean, nullable=False, default=False)
+ #: Required mode signals to UI that if this label is a parent, one of its
+ #: children must be mandatorily applied to the proposal. The value of this
+ #: field must be ignored if the label is not a parent
+ _required = db.Column('required', db.Boolean, nullable=False, default=False)
- #: Archived mode specifies that the labelset is no longer available for use
+ #: Archived mode specifies that the label is no longer available for use
#: although all the previous records will stay in database.
- archived = db.Column(db.Boolean, nullable=False, default=False)
+ _archived = db.Column('archived', db.Boolean, nullable=False, default=False)
- __table_args__ = (db.UniqueConstraint('project_id', 'name'),)
+ #: Proposals that this label is attached to
+ proposals = db.relationship(Proposal, secondary=proposal_label, lazy='dynamic', backref='labels')
- def __repr__(self):
- return "" % (self.name, self.project.name)
+ __table_args__ = (db.UniqueConstraint('project_id', 'name'),)
__roles__ = {
'all': {
'read': {
- 'name', 'title', 'project_id', 'seq', 'description', 'radio_code',
- 'restricted', 'required'
+ 'name', 'title', 'project_id', 'project', 'seq',
+ 'restricted', 'required', 'archived'
}
}
}
@hybrid_property
- def form_name(self):
- """
- Generates a name to be used in forms when a field is created for this labelset
- """
- return 'labelset_' + self.name.replace('-', '_')
+ def restricted(self):
+ return self.parent_label._restricted if self.parent_label else self._restricted
- @form_name.expression
- def form_name(cls):
- return func.concat('labelset_', func.replace(cls.name, '-', '_'))
-
- def roles_for(self, actor=None, anchors=()):
- roles = super(Labelset, self).roles_for(actor, anchors)
- roles.update(self.project.roles_for(actor, anchors))
- return roles
-
- @with_roles(call={'admin'})
- def assign_label(self, label):
- """
- This function takes a Label object and links the labelset with it.
- This function requires role control. Hence this function must be called
- via ``current_access()``.
-
- :param label: A Label instance
- """
- self.labels.append(label)
-
-
-proposal_label = db.Table(
- 'proposal_label', db.Model.metadata,
- db.Column('proposal_id', None, db.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False, primary_key=True),
- db.Column('label_id', None, db.ForeignKey('label.id', ondelete='CASCADE'), nullable=False, primary_key=True, index=True),
- db.Column('created_at', db.DateTime, default=db.func.utcnow())
-)
-
-
-class Label(BaseScopedNameMixin, db.Model):
- __tablename__ = 'label'
-
- labelset_id = db.Column(None, db.ForeignKey('labelset.id', ondelete='CASCADE'), nullable=False)
- labelset = db.relationship(Labelset)
- parent = db.synonym('labelset')
-
- #: Color code to be used in UI with this label
- bgcolor = db.Column(db.Unicode(6), nullable=False, default=u"CCCCCC")
-
- #: Sequence number for this label, used in UI for ordering
- seq = db.Column(db.Integer, nullable=False)
+ @hybrid_property
+ def archived(self):
+ return self._archived or self.parent_label._archived if self.parent_label else False
- #: Icon for displaying in space-constrained UI. Contains emoji
- icon_emoji = db.Column(db.Unicode(1), nullable=True)
+ # TODO: setter and expression for :meth:`restricted`, :meth:`archived`
- #: Archived mode specifies that the label is no longer available for use
- #: although all the previous records will stay in database.
- archived = db.Column(db.Boolean, nullable=False, default=False)
+ @hybrid_property
+ def is_parent(self):
+ return len(self.children) != 0
- #: Proposals that this label is attached to
- proposals = db.relationship(Proposal, secondary=proposal_label, lazy='dynamic', backref='labels')
+ # TODO: Check whether this expression works
+ @is_parent.expression
+ def is_parent(cls):
+ return exists().where(Label._parent_label_id == cls.id)
- __table_args__ = (db.UniqueConstraint('labelset_id', 'name'),)
+ @hybrid_property
+ def required(self):
+ return self._required
- def __repr__(self):
- return "" % (self.labelset.name, self.name)
+ @required.setter
+ def required(self, value):
+ if value and not self.children:
+ raise ValueError("Label without children cannot be required")
+ self._required = value
@property
def icon(self):
@@ -140,3 +124,11 @@ def icon(self):
if len(result) <= 1:
result = self.title.strip()[:3]
return result
+
+ def __repr__(self):
+ return "" % (self.labelset.name, self.name)
+
+ def roles_for(self, actor=None, anchors=()):
+ roles = super(Label, self).roles_for(actor, anchors)
+ roles.update(self.project.roles_for(actor, anchors))
+ return roles
diff --git a/funnel/models/project.py b/funnel/models/project.py
index 9de8795a0..a47dbe2cd 100644
--- a/funnel/models/project.py
+++ b/funnel/models/project.py
@@ -132,8 +132,10 @@ class Project(UuidMixin, BaseScopedNameMixin, db.Model):
venues = db.relationship('Venue', cascade='all, delete-orphan',
order_by='Venue.seq', collection_class=ordering_list('seq', count_from=1))
- labelsets = db.relationship('Labelset', cascade='all, delete-orphan',
- order_by='Labelset.seq', collection_class=ordering_list('seq', count_from=1))
+ labels = db.relationship('Label', cascade='all, delete-orphan',
+ primaryjoin='and_(Label.project_id == Project.id, Label.parent_label_id == None)',
+ order_by='Label.seq', collection_class=ordering_list('seq', count_from=1))
+ all_labels = db.relationship('Label', lazy='dynamic')
featured_sessions = db.relationship(
'Session',
From b733c6b1f55a6808ddc2cbe8e548e6d70aba2b1d Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Mon, 15 Apr 2019 19:19:14 +0530
Subject: [PATCH 027/125] Use self.is_parent
---
funnel/models/label.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 64cc5a98a..12cc91f49 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -106,7 +106,7 @@ def required(self):
@required.setter
def required(self, value):
- if value and not self.children:
+ if value and not self.is_parent:
raise ValueError("Label without children cannot be required")
self._required = value
From c3f61e6ad498900f41b0c00e24a50c616fb929a7 Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Mon, 15 Apr 2019 19:22:43 +0530
Subject: [PATCH 028/125] Required flag must be false if not a parent label
---
funnel/models/label.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 12cc91f49..fb3ba0ced 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -102,7 +102,7 @@ def is_parent(cls):
@hybrid_property
def required(self):
- return self._required
+ return self._required if self.is_parent else False
@required.setter
def required(self, value):
From c838cf31e7af77ca87a3a0e4548c51259ef8938f Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Tue, 16 Apr 2019 12:10:06 +0530
Subject: [PATCH 029/125] Updated docstrings
---
funnel/models/label.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index fb3ba0ced..fa26264e7 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -25,6 +25,8 @@ class Label(BaseScopedNameMixin, db.Model):
# `parent` is required for :meth:`~coaster.sqlalchemy.mixins.BaseScopedNameMixin.make_name()`
parent = db.synonym('project')
+ #: Parent label's id. Do not write to this column directly, as we don't have the ability to
+ #: validate the value within the app. Always use the :attr:`parent_label` relationship.
_parent_label_id = db.Column(
'parent_label_id',
None,
@@ -54,8 +56,9 @@ class Label(BaseScopedNameMixin, db.Model):
#: limit imposed here
icon_emoji = db.Column(db.UnicodeText, nullable=True)
- #: Restricted mode specifies that labels in this set may only be applied by someone with
- #: an editorial role (TODO: name the role)
+ #: Restricted mode specifies that this label may only be applied by someone with
+ #: an editorial role (TODO: name the role). If this label is a parent, it applies
+ #: to all its children
_restricted = db.Column('restricted', db.Boolean, nullable=False, default=False)
#: Required mode signals to UI that if this label is a parent, one of its
From 81f65a3ba1052399d5747d9d7fcb78ff6a384deb Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Tue, 16 Apr 2019 12:17:04 +0530
Subject: [PATCH 030/125] Don't need use_alter for self-referential
---
funnel/models/label.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index fa26264e7..0e6f3d6ab 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -30,7 +30,7 @@ class Label(BaseScopedNameMixin, db.Model):
_parent_label_id = db.Column(
'parent_label_id',
None,
- db.ForeignKey('label.id', ondelete='CASCADE', use_alter=True, name='label_parent_label_id_fkey'),
+ db.ForeignKey('label.id', ondelete='CASCADE'),
index=True,
nullable=True
)
From ba9670c96a7ed82624a9e3653826d1877be10cf7 Mon Sep 17 00:00:00 2001
From: Kiran Jonnalagadda
Date: Tue, 16 Apr 2019 12:17:33 +0530
Subject: [PATCH 031/125] Rename part_labels (column rename also needed)
---
funnel/models/project.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/funnel/models/project.py b/funnel/models/project.py
index a47dbe2cd..d8c9600fa 100644
--- a/funnel/models/project.py
+++ b/funnel/models/project.py
@@ -128,7 +128,7 @@ class Project(UuidMixin, BaseScopedNameMixin, db.Model):
parent_id = db.Column(None, db.ForeignKey('project.id', ondelete='SET NULL'), nullable=True)
parent_project = db.relationship('Project', remote_side='Project.id', backref='subprojects')
inherit_sections = db.Column(db.Boolean, default=True, nullable=False)
- labels = db.Column(JsonDict, nullable=False, server_default='{}')
+ part_labels = db.Column('labels', JsonDict, nullable=False, server_default='{}')
venues = db.relationship('Venue', cascade='all, delete-orphan',
order_by='Venue.seq', collection_class=ordering_list('seq', count_from=1))
@@ -358,11 +358,11 @@ def location_geonameid(self):
@property
def proposal_part_a(self):
- return self.labels.get('proposal', {}).get('part_a', {})
+ return self.part_labels.get('proposal', {}).get('part_a', {})
@property
def proposal_part_b(self):
- return self.labels.get('proposal', {}).get('part_b', {})
+ return self.part_labels.get('proposal', {}).get('part_b', {})
def set_labels(self, value=None):
"""
@@ -373,9 +373,9 @@ def set_labels(self, value=None):
are allowed to be customized per project.
"""
if value and isinstance(value, dict):
- self.labels = value
+ self.part_labels = value
else:
- self.labels = {
+ self.part_labels = {
"proposal": {
"part_a": {
"title": "Abstract",
From 9b5740e08685ca47a3dc77888292d04895c62ba4 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 12:19:59 +0530
Subject: [PATCH 032/125] added setter and expression for restricted and
archived
---
funnel/models/label.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index fb3ba0ced..379461bb5 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -85,10 +85,26 @@ class Label(BaseScopedNameMixin, db.Model):
def restricted(self):
return self.parent_label._restricted if self.parent_label else self._restricted
+ @restricted.setter
+ def restricted(self, value):
+ self._restricted = value
+
+ @restricted.expression
+ def restricted(cls):
+ return cls._restricted == True # NOQA
+
@hybrid_property
def archived(self):
return self._archived or self.parent_label._archived if self.parent_label else False
+ @archived.setter
+ def archived(self, value):
+ self._archived = value
+
+ @archived.expression
+ def archived(cls):
+ return cls._archived == True # NOQA
+
# TODO: setter and expression for :meth:`restricted`, :meth:`archived`
@hybrid_property
From 12a2d17e0f57d2cc42ebde32b9b39a0668314894 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 13:53:34 +0530
Subject: [PATCH 033/125] modified migration for labels
---
funnel/models/label.py | 12 +-
funnel/models/proposal.py | 12 +-
...abelsets.py => 0b25df40d307_add_labels.py} | 35 ++--
.../f89e3a85ed10_migrate_old_labels.py | 166 ------------------
runtests.sh | 1 +
tests/conftest.py | 47 ++---
tests/unit/models/test_label.py | 62 +++----
7 files changed, 82 insertions(+), 253 deletions(-)
rename migrations/versions/{0b25df40d307_add_labels_and_labelsets.py => 0b25df40d307_add_labels.py} (59%)
delete mode 100644 migrations/versions/f89e3a85ed10_migrate_old_labels.py
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 943203169..604c24a6f 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -27,7 +27,7 @@ class Label(BaseScopedNameMixin, db.Model):
#: Parent label's id. Do not write to this column directly, as we don't have the ability to
#: validate the value within the app. Always use the :attr:`parent_label` relationship.
- _parent_label_id = db.Column(
+ parent_label_id = db.Column(
'parent_label_id',
None,
db.ForeignKey('label.id', ondelete='CASCADE'),
@@ -49,7 +49,7 @@ class Label(BaseScopedNameMixin, db.Model):
seq = db.Column(db.Integer, nullable=False)
# A single-line description of this label, shown when picking labels (optional)
- description = db.Column(db.UnicodeText, nullable=False)
+ description = db.Column(db.UnicodeText, nullable=False, default=u"")
#: Icon for displaying in space-constrained UI. Contains one emoji symbol.
#: Since emoji can be composed from multiple symbols, there is no length
@@ -108,13 +108,10 @@ def archived(self, value):
def archived(cls):
return cls._archived == True # NOQA
- # TODO: setter and expression for :meth:`restricted`, :meth:`archived`
-
@hybrid_property
def is_parent(self):
return len(self.children) != 0
- # TODO: Check whether this expression works
@is_parent.expression
def is_parent(cls):
return exists().where(Label._parent_label_id == cls.id)
@@ -145,7 +142,10 @@ def icon(self):
return result
def __repr__(self):
- return "" % (self.labelset.name, self.name)
+ if self.parent_label:
+ return "" % (self.parent_label.name, self.name)
+ else:
+ return "" % self.name
def roles_for(self, actor=None, anchors=()):
roles = super(Label, self).roles_for(actor, anchors)
diff --git a/funnel/models/proposal.py b/funnel/models/proposal.py
index 65d151aa5..a0f9fbf50 100644
--- a/funnel/models/proposal.py
+++ b/funnel/models/proposal.py
@@ -334,6 +334,7 @@ def permissions(self, user, inherited=None):
'vote_proposal',
'new_comment',
'vote_comment',
+ 'edit-proposal'
])
if user == self.owner:
perms.update([
@@ -364,14 +365,17 @@ def assign_label(self, label):
"""
if label in self.labels:
return
- if label.labelset.radio_mode:
- existing_labels = set(label.labelset.labels).intersection(set(self.labels))
+ if label.is_parent:
+ raise ValueError("Parent labels cannot be assigned to a proposal")
+
+ if label.parent_label is not None:
+ existing_labels = set(label.parent_label.children).intersection(set(self.labels))
if existing_labels:
# the labelset is in radio mode and one of it's labels are
# already assigned to this proposal. We need to
# remove the older label and assign given label.
- existing_label = existing_labels.pop()
- self.labels.remove(existing_label)
+ for elabel in existing_labels:
+ self.labels.remove(elabel)
# labelset is not in radio mode, so we can assign label to proposal
self.labels.append(label)
diff --git a/migrations/versions/0b25df40d307_add_labels_and_labelsets.py b/migrations/versions/0b25df40d307_add_labels.py
similarity index 59%
rename from migrations/versions/0b25df40d307_add_labels_and_labelsets.py
rename to migrations/versions/0b25df40d307_add_labels.py
index dfd75808b..f4c6b3dbe 100644
--- a/migrations/versions/0b25df40d307_add_labels_and_labelsets.py
+++ b/migrations/versions/0b25df40d307_add_labels.py
@@ -1,7 +1,7 @@
-"""add labels and labelsets
+"""add labels
-Revision ID: 0b25df40d307
-Revises: ef93d256a8cf
+# Revision ID: 0b25df40d307
+# Revises: ef93d256a8cf
Create Date: 2019-04-08 14:44:05.164533
"""
@@ -15,37 +15,23 @@
def upgrade():
- op.create_table('labelset',
- sa.Column('created_at', sa.DateTime(), nullable=False),
- sa.Column('updated_at', sa.DateTime(), nullable=False),
- sa.Column('project_id', sa.Integer(), nullable=False),
- sa.Column('seq', sa.Integer(), nullable=False),
- sa.Column('description', sa.UnicodeText(), nullable=False),
- sa.Column('radio_mode', sa.Boolean(), nullable=False),
- sa.Column('restricted', sa.Boolean(), nullable=False),
- sa.Column('required', sa.Boolean(), nullable=False),
- sa.Column('archived', sa.Boolean(), nullable=False),
- sa.Column('name', sa.Unicode(length=250), nullable=False),
- sa.Column('title', sa.Unicode(length=250), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('project_id', 'name')
- )
op.create_table('label',
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
- sa.Column('labelset_id', sa.Integer(), nullable=False),
+ sa.Column('parent_label_id', sa.Integer(), nullable=False),
sa.Column('seq', sa.Integer(), nullable=False),
+ sa.Column('description', sa.UnicodeText(), nullable=False),
sa.Column('bgcolor', sa.Unicode(length=6), nullable=False),
- sa.Column('icon_emoji', sa.Unicode(length=1), nullable=True),
+ sa.Column('icon_emoji', sa.UnicodeText(), nullable=True),
sa.Column('archived', sa.Boolean(), nullable=False),
+ sa.Column('restricted', sa.Boolean(), nullable=False),
+ sa.Column('required', sa.Boolean(), nullable=False),
sa.Column('name', sa.Unicode(length=250), nullable=False),
sa.Column('title', sa.Unicode(length=250), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['labelset_id'], ['labelset.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['parent_label_id'], ['label.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('labelset_id', 'name')
+ sa.UniqueConstraint('parent_label_id', 'name')
)
op.create_table('proposal_label',
sa.Column('proposal_id', sa.Integer(), nullable=False),
@@ -62,4 +48,3 @@ def downgrade():
op.drop_index(op.f('ix_proposal_label_label_id'), table_name='proposal_label')
op.drop_table('proposal_label')
op.drop_table('label')
- op.drop_table('labelset')
diff --git a/migrations/versions/f89e3a85ed10_migrate_old_labels.py b/migrations/versions/f89e3a85ed10_migrate_old_labels.py
deleted file mode 100644
index 284151684..000000000
--- a/migrations/versions/f89e3a85ed10_migrate_old_labels.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""migrate old labels
-
-Revision ID: f89e3a85ed10
-Revises: 0b25df40d307
-Create Date: 2019-04-11 14:42:55.182622
-
-"""
-
-# revision identifiers, used by Alembic.
-revision = 'f89e3a85ed10'
-down_revision = '0b25df40d307'
-
-from datetime import datetime
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.sql import table, column
-
-
-proposal = table('proposal',
- column('id', sa.Integer()),
- column('project_id', sa.Integer()),
- column('technical_level', sa.Unicode(40)),
- column('session_type', sa.Unicode(40)),
- column('section_id', sa.Integer()),
- )
-
-project = table('project',
- column('id', sa.Integer()),
- column('name', sa.Unicode(250))
- )
-
-section = table('section',
- column('id', sa.Integer()),
- column('project_id', sa.Integer()),
- column('name', sa.Unicode(250)),
- column('title', sa.Unicode(250)),
- column('description', sa.UnicodeText()),
- column('created_at', sa.DateTime()),
- column('updated_at', sa.DateTime())
- )
-
-labelset = table('labelset',
- column('id', sa.Integer()),
- column('name', sa.Unicode(250)),
- column('title', sa.Unicode(250)),
- column('project_id', sa.Integer()),
- column('seq', sa.Integer()),
- column('description', sa.UnicodeText()),
- column('radio_mode', sa.Boolean()),
- column('restricted', sa.Boolean()),
- column('archived', sa.Boolean()),
- column('required', sa.Boolean()),
- column('created_at', sa.DateTime()),
- column('updated_at', sa.DateTime())
- )
-
-label = table('label',
- column('id', sa.Integer()),
- column('name', sa.Unicode(250)),
- column('title', sa.Unicode(250)),
- column('bgcolor', sa.Unicode(6)),
- column('labelset_id', sa.Integer()),
- column('seq', sa.Integer()),
- column('archived', sa.Boolean()),
- column('created_at', sa.DateTime()),
- column('updated_at', sa.DateTime())
- )
-
-proposal_label = table('proposal_label',
- column('proposal_id', sa.Integer()),
- column('label_id', sa.Integer()),
- column('created_at', sa.DateTime())
- )
-
-
-def upgrade():
- conn = op.get_bind()
- # Migrate sections -
- projects = conn.execute(project.select())
- for proj in projects:
- sec_count = conn.scalar(
- sa.select([sa.func.count('*')]).select_from(section).where(section.c.project_id == proj['id'])
- )
- if sec_count > 0:
- # the project has some sections
- # create section labelset for the project
- labset = conn.execute(labelset.insert().values({
- 'project_id': proj['id'], 'name': u"section", 'title': u"Section",
- 'seq': 1, 'description': "", 'radio_mode': True, 'restricted': True,
- 'required': True, 'created_at': datetime.now(), 'updated_at': datetime.now(),
- 'archived': False
- }).returning(labelset.c.id)).first()
-
- # add old sections as labels to the above labelset
- sections = conn.execute(section.select().where(section.c.project_id == proj['id']))
- for index, sec in enumerate(sections, start=1):
- lab = conn.execute(label.insert().values({
- 'name': sec['name'], 'title': sec['title'], 'archived': False,
- 'labelset_id': labset[0], 'seq': index, 'bgcolor': u"CCCCCC",
- 'created_at': datetime.now(), 'updated_at': datetime.now()
- }).returning(label.c.id, label.c.name)).first()
-
- # only select proposals that belonged to above section and
- # still in the same project as the section. This removed proposals
- # that were moved to other projects but their section relationship
- # was never migrated.
- proposals = conn.execute(
- proposal.select().where(proposal.c.section_id == sec['id']).where(proposal.c.project_id == sec['project_id'])
- )
- for prop in proposals:
- pl = conn.execute(proposal_label.insert().values({
- 'proposal_id': prop['id'], 'label_id': lab['id'], 'created_at': datetime.now()
- }))
-
- # technical level
- labset = conn.execute(labelset.insert().values({
- 'project_id': proj['id'], 'name': u"technical-level", 'title': u"Technical Level",
- 'seq': 1, 'description': "", 'radio_mode': True, 'restricted': True, 'archived': False,
- 'required': True, 'created_at': datetime.now(), 'updated_at': datetime.now()
- }).returning(labelset.c.id)).first()
- tl_list = [('beginner', "Beginner"), ('intermediate', "Intermediate"), ('advanced', "Advanced")]
- for index, tl in enumerate(tl_list):
- tl_name, tl_title = tl
- lab = conn.execute(label.insert().values({
- 'name': tl_name, 'title': tl_title, 'archived': False,
- 'labelset_id': labset[0], 'seq': index, 'bgcolor': u"CCCCCC",
- 'created_at': datetime.now(), 'updated_at': datetime.now()
- }).returning(label.c.id, label.c.name)).first()
-
- proposals = conn.execute(
- proposal.select().where(proposal.c.project_id == proj['id']).where(proposal.c.technical_level == tl_title)
- )
- for prop in proposals:
- pl = conn.execute(proposal_label.insert().values({
- 'proposal_id': prop['id'], 'label_id': lab['id'], 'created_at': datetime.now()
- }))
-
- # session type
- labset = conn.execute(labelset.insert().values({
- 'project_id': proj['id'], 'name': u"session-type", 'title': u"Session Type",
- 'seq': 1, 'description': "", 'radio_mode': True, 'restricted': True, 'archived': False,
- 'required': True, 'created_at': datetime.now(), 'updated_at': datetime.now()
- }).returning(labelset.c.id)).first()
- st_list = [('lecture', "Lecture"), ('demo', "Demo"), ('tutorial', "Tutorial"), ('workshop', "Workshop"), ('discussion', "Discussion"), ('panel', "Panel")]
- for index, st in enumerate(st_list):
- st_name, st_title = st
- lab = conn.execute(label.insert().values({
- 'name': st_name, 'title': st_title, 'archived': False,
- 'labelset_id': labset[0], 'seq': index, 'bgcolor': u"CCCCCC",
- 'created_at': datetime.now(), 'updated_at': datetime.now()
- }).returning(label.c.id, label.c.name)).first()
-
- proposals = conn.execute(
- proposal.select().where(proposal.c.project_id == proj['id']).where(proposal.c.session_type == st_title)
- )
- for prop in proposals:
- conn.execute(proposal_label.insert().values({
- 'proposal_id': prop['id'], 'label_id': lab['id'], 'created_at': datetime.now()
- }))
-
-
-def downgrade():
- conn = op.get_bind()
- conn.execute(labelset.delete())
- conn.execute(label.delete())
- conn.execute(proposal_label.delete())
diff --git a/runtests.sh b/runtests.sh
index 3bbd3747c..594fb3abc 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -1,5 +1,6 @@
#!/bin/sh
set -e
+export PYTHONIOENCODING="UTF-8"
export FLASK_ENV="TESTING"
coverage run -m pytest "$@"
coverage report -m
diff --git a/tests/conftest.py b/tests/conftest.py
index 886d90d49..0af1088bb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,10 +4,9 @@
import pytest
import uuid
import coaster.app
-from coaster.auth import add_auth_attribute
from flask_lastuser import Lastuser
from flask_lastuser.sqlalchemy import UserManager
-from funnel.models import db, Profile, Project, User, Label, Labelset, Proposal, Team
+from funnel.models import db, Profile, Project, User, Label, Proposal, Team
flask_app = Flask(__name__, instance_relative_config=True)
@@ -130,36 +129,42 @@ def new_project(test_db, new_profile, new_user, new_team):
# so that changes made to the objects they return in one test function
# doesn't affect another test function.
+
@pytest.fixture(scope='function')
-def new_labelset(test_db, new_project):
- labelset_a = Labelset(
- title=u"Labelset A", project=new_project,
- description=u"A test labelset", radio_mode=False,
- restricted=False, required=False
+def new_parent_label(test_db, new_project):
+ parent_label_a = Label(
+ title=u"Parent Label A", project=new_project,
+ description=u"A test parent label"
)
- new_project.labelsets.append(labelset_a)
- test_db.session.add(labelset_a)
- test_db.session.commit()
+ new_project.labels.append(parent_label_a)
+ test_db.session.add(parent_label_a)
- label_a1 = Label(
- title=u"Label A1", icon_emoji=u"👍", labelset=labelset_a
- )
- labelset_a.labels.append(label_a1)
+ label_a1 = Label(title=u"Label A1", icon_emoji=u"👍", project=new_project)
test_db.session.add(label_a1)
- test_db.session.commit()
- label_a2 = Label(
- title=u"Label A2", labelset=labelset_a
- )
- labelset_a.labels.append(label_a2)
+ label_a2 = Label(title=u"Label A2", project=new_project)
test_db.session.add(label_a2)
+
+ parent_label_a.children.append(label_a1)
+ parent_label_a.children.append(label_a2)
+ parent_label_a.required = True
+ parent_label_a.restricted = True
test_db.session.commit()
- return labelset_a
+ return parent_label_a
+
+
+@pytest.fixture(scope='function')
+def new_label(test_db, new_project):
+ label_b = Label(title=u"Label B", icon_emoji=u"🔟", project=new_project)
+ new_project.labels.append(label_b)
+ test_db.session.add(label_b)
+ test_db.session.commit()
+ return label_b
@pytest.fixture(scope='function')
-def new_proposal(test_db, new_user, new_project, new_labelset):
+def new_proposal(test_db, new_user, new_project):
proposal = Proposal(
user=new_user, speaker=new_user, project=new_project,
title=u"Test Proposal", description=u"Test proposal description",
diff --git a/tests/unit/models/test_label.py b/tests/unit/models/test_label.py
index 4fd8ec055..b83679101 100644
--- a/tests/unit/models/test_label.py
+++ b/tests/unit/models/test_label.py
@@ -1,45 +1,45 @@
# -*- coding: utf-8 -*-
+import pytest
+
class TestLabels(object):
- def test_labelset_from_fixture(self, test_client, test_db, new_labelset):
- assert new_labelset.title == u"Labelset A"
- assert new_labelset.name == u"labelset-a"
- assert new_labelset.seq == 1
-
- def test_label_from_fixture(self, test_client, test_db, new_labelset):
- assert len(new_labelset.labels) > 0
- label_a1 = new_labelset.labels[0]
+ def test_parent_label_from_fixture(self, test_client, new_parent_label):
+ assert new_parent_label.title == u"Parent Label A"
+ assert new_parent_label.seq == 1
+ assert new_parent_label.is_parent
+ assert new_parent_label.required
+ assert new_parent_label.restricted
+ assert not new_parent_label.archived
+ assert len(new_parent_label.children) > 0
+
+ def test_child_label_from_fixture(self, test_client, test_db, new_parent_label):
+ assert len(new_parent_label.children) > 0
+ label_a1 = new_parent_label.children[0]
assert label_a1.title == u"Label A1"
- assert label_a1.name == u"label-a1"
assert label_a1.icon_emoji == u"👍"
assert label_a1.icon == u"👍"
-
- def test_proposal_assignment(self, test_client, test_db, new_labelset, new_proposal):
- label_a1 = new_labelset.labels[0]
- label_a2 = new_labelset.labels[1]
- new_proposal.assign_label(label_a1)
- assert label_a1 in new_proposal.labels
-
- new_proposal.assign_label(label_a2)
- # because labelset_a is not in radio mode,
- # both labels will exist
- assert label_a1 in new_proposal.labels
- assert label_a2 in new_proposal.labels
-
- def test_proposal_assignment_radio(self, test_client, test_db, new_labelset, new_proposal):
- new_labelset.radio_mode = True
- test_db.session.add(new_labelset)
- test_db.session.commit()
-
- label_a1 = new_labelset.labels[0]
- label_a2 = new_labelset.labels[1]
+ assert not label_a1.is_parent
+
+ def test_label_from_fixture(self, test_client, test_db, new_label):
+ assert new_label.title == u"Label B"
+ assert new_label.icon_emoji == u"🔟"
+ assert new_label.icon == u"🔟"
+ assert not new_label.is_parent
+
+ with pytest.raises(ValueError):
+ # because Label B is not a parent label, it cannot be required
+ new_label.required = True
+
+ def test_proposal_assignment_radio(self, test_client, test_db, new_parent_label, new_proposal):
+ # Parent labels are always in radio mode
+ label_a1 = new_parent_label.children[0]
+ label_a2 = new_parent_label.children[1]
new_proposal.assign_label(label_a1)
assert label_a1 in new_proposal.labels
new_proposal.assign_label(label_a2)
- # because labelset_a is in radio mode,
+ # because new_parent_label is in radio mode,
# label_a2 will replace label_a1
- # label_a1 will not exist in ``.labels`
assert label_a1 not in new_proposal.labels
assert label_a2 in new_proposal.labels
From 10da32c4be199d1aa2f3a09459d5d42468e326f9 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 16:54:18 +0530
Subject: [PATCH 034/125] fixed expression for flags
---
funnel/models/label.py | 20 +++++++++++++++++---
tests/conftest.py | 12 ++++++------
tests/unit/models/test_label.py | 6 ++++++
3 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 604c24a6f..cd8ebb465 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-from sqlalchemy.sql import exists
+from sqlalchemy.sql import case, exists
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.hybrid import hybrid_property
@@ -90,11 +90,15 @@ def restricted(self):
@restricted.setter
def restricted(self, value):
+ if self.parent_label:
+ raise ValueError("Cannot restrict a child label")
self._restricted = value
@restricted.expression
def restricted(cls):
- return cls._restricted == True # NOQA
+ return case([
+ (cls.parent_label_id != None, db.select([Label._restricted]).where(Label.id == cls.parent_label_id).as_scalar()) # NOQA
+ ], else_=cls._restricted)
@hybrid_property
def archived(self):
@@ -102,11 +106,15 @@ def archived(self):
@archived.setter
def archived(self, value):
+ if self.parent_label:
+ raise ValueError("Cannot archive a child label")
self._archived = value
@archived.expression
def archived(cls):
- return cls._archived == True # NOQA
+ return case([
+ (cls.parent_label_id != None, db.select([Label._archived]).where(Label.id == cls.parent_label_id).as_scalar()) # NOQA
+ ], else_=cls._archived)
@hybrid_property
def is_parent(self):
@@ -126,6 +134,12 @@ def required(self, value):
raise ValueError("Label without children cannot be required")
self._required = value
+ @archived.expression
+ def archived(cls):
+ return case([
+ (cls.parent_label_id != None, db.select([Label._required]).where(Label.id == cls.parent_label_id).as_scalar()) # NOQA
+ ], else_=cls._required)
+
@property
def icon(self):
"""
diff --git a/tests/conftest.py b/tests/conftest.py
index 0af1088bb..b6808e956 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -124,13 +124,8 @@ def new_project(test_db, new_profile, new_user, new_team):
test_db.session.commit()
return project
-# Scope: function
-# These fixtures are run before every test function,
-# so that changes made to the objects they return in one test function
-# doesn't affect another test function.
-
-@pytest.fixture(scope='function')
+@pytest.fixture(scope='session')
def new_parent_label(test_db, new_project):
parent_label_a = Label(
title=u"Parent Label A", project=new_project,
@@ -153,6 +148,11 @@ def new_parent_label(test_db, new_project):
return parent_label_a
+# Scope: function
+# These fixtures are run before every test function,
+# so that changes made to the objects they return in one test function
+# doesn't affect another test function.
+
@pytest.fixture(scope='function')
def new_label(test_db, new_project):
diff --git a/tests/unit/models/test_label.py b/tests/unit/models/test_label.py
index b83679101..fae6ec9ec 100644
--- a/tests/unit/models/test_label.py
+++ b/tests/unit/models/test_label.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import pytest
+from funnel.models import Label
class TestLabels(object):
@@ -43,3 +44,8 @@ def test_proposal_assignment_radio(self, test_client, test_db, new_parent_label,
# label_a2 will replace label_a1
assert label_a1 not in new_proposal.labels
assert label_a2 in new_proposal.labels
+
+ def test_label_flags(self, new_parent_label, new_label):
+ restricted_labels = Label.query.filter(Label.restricted == True).all()
+ assert new_parent_label in restricted_labels
+ assert new_label not in restricted_labels
From d403b22a840ac202d690e1a90621f00f462b2de8 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 17:24:49 +0530
Subject: [PATCH 035/125] replaced labelset views and form
---
funnel/forms/proposal.py | 81 +++++---------------------
funnel/views/label.py | 123 ++++++---------------------------------
funnel/views/proposal.py | 13 ++---
3 files changed, 37 insertions(+), 180 deletions(-)
diff --git a/funnel/forms/proposal.py b/funnel/forms/proposal.py
index 40753464b..052dd2370 100644
--- a/funnel/forms/proposal.py
+++ b/funnel/forms/proposal.py
@@ -3,62 +3,25 @@
from baseframe import __
import baseframe.forms as forms
from flask import g
-from baseframe.forms.sqlalchemy import QuerySelectField
-from ..models import Project, Profile, Label, Labelset
+from baseframe.forms.sqlalchemy import QuerySelectField, QuerySelectMultipleField
+from ..models import Project, Profile, Label
__all__ = ['TransferProposal', 'ProposalForm', 'ProposalTransitionForm',
- 'ProposalMoveForm', 'get_proposal_form', 'ProposalLabelsetBaseForm']
+ 'ProposalMoveForm', 'ProposalLabelsForm']
class TransferProposal(forms.Form):
userid = forms.UserSelectField(__("Transfer to"), validators=[forms.validators.DataRequired()])
-class ProposalLabelsetBaseForm(forms.Form):
- """
- This base form provides the `set_queries()` and `populate_obj_labels` methods
- that lets you fill a proposal form with the labels and then save them.
+class ProposalLabelsForm(forms.Form):
+ labels = QuerySelectMultipleField(__("Labels"), get_label='title', get_pk=lambda l: l.name)
- Any Proposal form that needs to show labels, need to inherit this class.
- """
def set_queries(self):
- if self.edit_parent is not None:
- # Fill up the choices for the labelsets
- for labelset in self.edit_parent.labelsets:
- labelset_field = getattr(self, labelset.form_name)
- labelset_field.choices = [(l.name, l.title) for l in labelset.labels]
- if self.edit_obj is not None:
- # If it's an edit form, select the proper label for each labelset
- labels_data = set(self.edit_obj.labels).intersection(set(labelset.labels))
- data = labels_data.pop().name if len(labels_data) == 1 else [l.name for l in labels_data]
- if labelset_field.data == 'None' and data:
- labelset_field.data = data
-
- def populate_obj_labels(self, proposal):
- """
- Assign the appropriate labels to the proposal
- """
- labelset_keys = [key for key in self.data.keys() if key.startswith('labelset_')]
- for key in labelset_keys:
- labelset = Labelset.query.filter_by(form_name=key, project=proposal.project).first()
- if labelset.radio_mode:
- # in case of RadioField, self.data.get(key) should be a single value
- new_label = Label.query.filter_by(labelset=labelset, name=self.data.get(key)).first()
- proposal.assign_label(new_label)
- else:
- # FIXME: Move this part inside model?
- existing_labels = set(labelset.labels).intersection(set(proposal.labels))
- # in case of MultiSelectField, self.data.get(key) should be a list
- label_names = self.data.get(key)
- new_labels = Label.query.filter(Label.labelset == labelset, Label.name.in_(label_names)).all()
- removed_labels = existing_labels.difference(set(new_labels))
- for rlabel in removed_labels:
- proposal.labels.remove(rlabel)
- for nlabel in new_labels:
- proposal.assign_label(nlabel)
-
-
-class ProposalForm(ProposalLabelsetBaseForm):
+ self.labels.query = self.edit_parent.labels
+
+
+class ProposalForm(forms.Form):
speaking = forms.RadioField(__("Are you speaking?"), coerce=int,
choices=[(1, __(u"I will be speaking")),
(0, __(u"I’m proposing a topic for someone to speak on"))])
@@ -93,6 +56,8 @@ class ProposalForm(ProposalLabelsetBaseForm):
location = forms.StringField(__("Your location"), validators=[forms.validators.DataRequired(), forms.validators.Length(max=80)],
description=__("Your location, to help plan for your travel if required"))
+ labels = QuerySelectMultipleField(__("Labels"), get_label='title', get_pk=lambda l: l.name)
+
def __init__(self, *args, **kwargs):
super(ProposalForm, self).__init__(*args, **kwargs)
project = kwargs.get('parent')
@@ -105,28 +70,8 @@ def __init__(self, *args, **kwargs):
if project.proposal_part_b.get('hint'):
self.description.description = project.proposal_part_b.get('hint')
-
-def get_proposal_form(base_form_class, *args, **kwargs):
- """
- Takes a proposal form class as base form and adds the labelset fields to it.
- Dynamic fields can only be added to the form class and not instance. Hence this.
- Any form that has `obj=` or `parent=`
- passed to it, can be used with this function.
- `parent` kwarg must be provided, otherwise the labelsets wont be added to the form.
- """
- if 'parent' in kwargs:
- # we need parent project to be able to handle labelsets
- project = kwargs.get('parent')
- for labelset in project.labelsets:
- ls_name = labelset.form_name
- if not hasattr(base_form_class, ls_name):
- if labelset.restricted and not set(project.current_roles).intersection({'admin', 'reviewer'}):
- continue
- FieldType = forms.RadioField if labelset.radio_mode else forms.SelectMultipleField
- validators = [forms.validators.DataRequired()] if labelset.required else []
- setattr(base_form_class, ls_name, FieldType(labelset.title, validators=validators,
- choices=[], description=labelset.description))
- return base_form_class(*args, **kwargs)
+ def set_queries(self):
+ self.labels.query = self.edit_parent.labels
class ProposalTransitionForm(forms.Form):
diff --git a/funnel/views/label.py b/funnel/views/label.py
index 67c5bc307..07d95eaa5 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -6,14 +6,14 @@
from baseframe.forms import render_delete_sqla, render_form
from .. import app, funnelapp, lastuser
-from ..models import Labelset, Label, db, Project, Profile
+from ..models import Label, db, Project, Profile
from ..forms import LabelsetForm, LabelForm
from .mixins import ProjectViewMixin
from .decorators import legacy_redirect
-@route('///labelsets')
-class ProjectLabelsetView(ProjectViewMixin, UrlForView, ModelView):
+@route('///labels')
+class ProjectLabelView(ProjectViewMixin, UrlForView, ModelView):
__decorators__ = [legacy_redirect]
@route('')
@@ -26,110 +26,33 @@ def labelsets(self):
@route('new', methods=['GET', 'POST'])
@lastuser.requires_login
@requires_permission('new_labelset')
- def new_labelset(self):
- form = LabelsetForm(model=Labelset, parent=self.obj)
- if form.validate_on_submit():
- labelset = Labelset(project=self.obj)
- form.populate_obj(labelset)
- if 'assign_labelset' in self.obj.current_access():
- self.obj.current_access().assign_labelset(labelset)
- db.session.commit()
- flash(_("The labelset has been successfully created. Assign some labels to it."), 'info')
- return redirect(self.obj.url_for('labelsets'), code=303)
- else:
- flash(_("You don't have permission to create a new labelset for this project"), 'danger')
- return redirect(self.obj.url_for('labelsets'), code=303)
- return render_form(form=form, title=_("New labelset"), submit=_("Create labelset"))
-
-
-@route('//labelsets', subdomain='')
-class FunnelProjectLabelsetView(ProjectLabelsetView):
- pass
-
-
-ProjectLabelsetView.init_app(app)
-FunnelProjectLabelsetView.init_app(funnelapp)
-
-
-@route('///labelsets/')
-class LabelsetView(UrlForView, ModelView):
- __decorators__ = [legacy_redirect]
- model = Labelset
- route_model_map = {'profile': 'project.profile.name', 'project': 'project.name', 'labelset': 'name'}
-
- def loader(self, profile, project, labelset):
- labelset = self.model.query.join(Project).join(Profile).filter(
- Project.name == project, Profile.name == profile,
- Labelset.name == labelset
- ).first_or_404()
- g.profile = labelset.project.profile
- return labelset
-
- @route('edit', methods=['GET', 'POST'])
- @lastuser.requires_login
- @requires_permission('edit_project')
- def edit(self):
- form = LabelsetForm(obj=self.obj, model=Labelset, parent=self.obj.parent)
- if form.validate_on_submit():
- form.populate_obj(self.obj)
- db.session.commit()
- flash(_("Your labelset has been edited"), 'info')
- return redirect(self.obj.project.url_for('labelsets'), code=303)
- return render_form(form=form, title=_("Edit labelset"), submit=_("Save changes"))
+ def new_label(self):
+ pass
- @route('delete', methods=['GET', 'POST'])
- @lastuser.requires_login
- @requires_permission('edit_project')
- def delete(self):
- return render_delete_sqla(self.obj, db, title=_(u"Confirm delete"),
- message=_(u"Do you really wish to delete labelset '{title}’?").format(title=self.obj.title),
- success=_("Your labelset has been deleted"),
- next=self.obj.project.url_for('labelsets'),
- cancel_url=self.obj.project.url_for('labelsets'))
- @route('new', methods=['GET', 'POST'])
- @lastuser.requires_login
- @requires_permission('new_labelset')
- def new_label(self):
- form = LabelForm(model=Label, parent=self.obj)
- if form.validate_on_submit():
- label = Label(labelset=self.obj)
- form.populate_obj(label)
- if 'assign_label' in self.obj.current_access():
- self.obj.current_access().assign_label(label)
- db.session.commit()
- flash(_("The label has been successfully created."), 'info')
- return redirect(self.obj.project.url_for('labelsets'), code=303)
- else:
- flash(_("You don't have permission to create a new label for this project"), 'danger')
- return redirect(self.obj.project.url_for('labelsets'), code=303)
- return render_form(form=form, title=_("New label for '{}'".format(self.obj.title)), submit=_("Create label"))
-
-
-@route('//labelset/', subdomain='')
-class FunnelLabelsetView(LabelsetView):
+@route('//labels', subdomain='')
+class FunnelProjectLabelView(ProjectLabelView):
pass
-LabelsetView.init_app(app)
-FunnelLabelsetView.init_app(funnelapp)
+ProjectLabelView.init_app(app)
+FunnelProjectLabelView.init_app(funnelapp)
-@route('///labelset//labels/')
+@route('///labels/')
class LabelView(UrlForView, ModelView):
__decorators__ = [lastuser.requires_login, legacy_redirect]
model = Label
route_model_map = {
- 'profile': 'labelset.project.profile.name', 'project': 'labelset.project.name',
- 'labelset': 'labelset.name', 'label': 'name'}
+ 'profile': 'project.profile.name', 'project': 'project.name',
+ 'label': 'name'}
- def loader(self, profile, project, labelset, label):
+ def loader(self, profile, project, label):
proj = Project.query.join(Profile).filter(
- Profile.name == profile, Project.name == project
+ Profile.name == profile, Project.name == project
).first_or_404()
- lset = Labelset.query.filter_by(project=proj, name=labelset).first_or_404()
- label = self.model.query.filter_by(labelset=lset, name=label).first_or_404()
- g.profile = label.labelset.project.profile
+ label = self.model.query.filter_by(project=proj, name=label).first_or_404()
+ g.profile = label.project.profile
return label
@route('edit', methods=['GET', 'POST'])
@@ -141,21 +64,11 @@ def edit(self):
form.populate_obj(self.obj)
db.session.commit()
flash(_("Your label has been edited"), 'info')
- return redirect(self.obj.labelset.project.url_for('labelsets'), code=303)
+ return redirect(self.obj.project.url_for('labelsets'), code=303)
return render_form(form=form, title=_("Edit label"), submit=_("Save changes"))
- @route('delete', methods=['GET', 'POST'])
- @lastuser.requires_login
- @requires_permission('edit_project')
- def delete(self):
- return render_delete_sqla(self.obj, db, title=_(u"Confirm delete"),
- message=_(u"Do you really wish to delete label '{title}’?").format(title=self.obj.title),
- success=_("Your label has been deleted"),
- next=self.obj.labelset.project.url_for('labelsets'),
- cancel_url=self.obj.labelset.project.url_for('labelsets'))
-
-@route('//labelset/', subdomain='')
+@route('//labels/', subdomain='')
class FunnelLabelView(LabelView):
pass
diff --git a/funnel/views/proposal.py b/funnel/views/proposal.py
index cbb79b5f7..6c92df3b9 100644
--- a/funnel/views/proposal.py
+++ b/funnel/views/proposal.py
@@ -4,7 +4,6 @@
from bleach import linkify
from flask import g, redirect, request, Markup, abort, flash, escape
-from sqlalchemy import or_
from coaster.utils import make_name
from coaster.views import ModelView, UrlChangeCheck, UrlForView, jsonp, render_with, requires_permission, route
from coaster.auth import current_auth
@@ -12,9 +11,9 @@
from baseframe.forms import render_form, render_delete_sqla, Form
from .. import app, funnelapp, lastuser
-from ..models import db, Section, Proposal, Comment, Label, Labelset
-from ..forms import (ProposalForm, CommentForm, DeleteCommentForm, ProposalLabelsetBaseForm,
- ProposalTransitionForm, ProposalMoveForm, get_proposal_form)
+from ..models import db, Proposal, Comment
+from ..forms import (ProposalForm, CommentForm, DeleteCommentForm,
+ ProposalTransitionForm, ProposalMoveForm, ProposalLabelsForm)
from .mixins import ProjectViewMixin, ProposalViewMixin
from .decorators import legacy_redirect
@@ -94,7 +93,7 @@ class BaseProjectProposalView(ProjectViewMixin, UrlChangeCheck, UrlForView, Mode
@lastuser.requires_login
@requires_permission('new-proposal')
def new_proposal(self):
- form = get_proposal_form(ProposalForm, model=Proposal, parent=self.obj)
+ form = ProposalForm(model=Proposal, parent=self.obj)
if request.method == 'GET':
form.email.data = g.user.email
form.phone.data = g.user.phone
@@ -168,7 +167,7 @@ def json(self):
@lastuser.requires_login
@requires_permission('edit-proposal')
def edit(self):
- form = get_proposal_form(ProposalForm, obj=self.obj.formdata, model=Proposal, parent=self.obj.project)
+ form = ProposalForm(obj=self.obj.formdata, model=Proposal, parent=self.obj.project)
if self.obj.user != g.user:
del form.speaking
if form.validate_on_submit():
@@ -256,7 +255,7 @@ def schedule(self):
@lastuser.requires_login
@requires_permission('admin')
def edit_labels(self):
- form = get_proposal_form(ProposalLabelsetBaseForm, obj=self.obj, parent=self.obj.project)
+ form = ProposalLabelsForm(model=Proposal, obj=self.obj, parent=self.obj.project)
if form.validate_on_submit():
form.populate_obj_labels(self.obj)
db.session.commit()
From ea255bc2e9b98d07ef6410888493518e0001ca95 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 18:14:00 +0530
Subject: [PATCH 036/125] removed labelset form
---
funnel/forms/label.py | 31 ++++++++-------------------
funnel/models/project.py | 12 -----------
funnel/models/proposal.py | 4 ++--
funnel/templates/macros.html.jinja2 | 2 +-
funnel/templates/proposal.html.jinja2 | 2 +-
funnel/views/label.py | 10 ++++-----
6 files changed, 18 insertions(+), 43 deletions(-)
diff --git a/funnel/forms/label.py b/funnel/forms/label.py
index c200a10a7..190bbe1b5 100644
--- a/funnel/forms/label.py
+++ b/funnel/forms/label.py
@@ -4,36 +4,23 @@
import baseframe.forms as forms
from .project import valid_color_re
-__all__ = ['LabelsetForm', 'LabelForm']
-
-
-class LabelsetForm(forms.Form):
- title = forms.StringField(__("Name"),
- description=__("Name of the labelset"),
- validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
- description = forms.MarkdownField(__("Description"), description=__("About the labelset"))
- required = forms.BooleanField(__("Required"), default=False,
- description=__("When required is set, this labelset must be set for a proposal (e.g. Proposal Type)."))
- radio_mode = forms.BooleanField(__("Radio mode"), default=False,
- description=__("When in radio mode, only one label within the labelset can be set at a time (e.g. Proposal Type)."))
- restricted = forms.BooleanField(__("Restricted"), default=False,
- description=__("When in restricted mode, only an admin or reviewer can set this labelset for a proposal (e.g. Proposal Status)."))
- archived = forms.BooleanField(__("Archived"), default=False,
- description=__("Once archived, this labelset will not be available for use in future, but the past records will be preserved."))
+__all__ = ['LabelForm']
class LabelForm(forms.Form):
title = forms.StringField(__("Name"), description=__("Name of the label"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
+ description = forms.MarkdownField(__("Description"), description=__("About the label"))
bgcolor = forms.StringField(__("Label Color"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=6)],
description=__("RGB Color for the label. Enter without the '#'. E.g. CCCCCC."), default=u"CCCCCC")
- archived = forms.BooleanField(__("Archived"), default=False,
- description=__("Once archived, this label will not be available for use in future, but the past records will be preserved."))
- # We're not going to use icon_emoji until we move to Py3
- # icon_emoji = forms.StringField(__("Icon/Emoji"),
- # validators=[forms.validators.Length(max=2)],
- # description=__("Emoji to be used for this label for space constrained UI"))
+ icon_emoji = forms.StringField(__("Icon/Emoji"),
+ validators=[forms.validators.Length(max=2)],
+ description=__("Emoji to be used for this label for space constrained UI"))
+ required = forms.BooleanField(__("Required"), default=False,
+ description=__("When required is set, this label must be set for a proposal (e.g. Proposal Type)."))
+ restricted = forms.BooleanField(__("Restricted"), default=False,
+ description=__("When in restricted mode, only an admin or reviewer can set this label for a proposal (e.g. Proposal Status)."))
def validate_bgcolor(self, field):
if not valid_color_re.match(field.data):
diff --git a/funnel/models/project.py b/funnel/models/project.py
index d8c9600fa..e0004c3ed 100644
--- a/funnel/models/project.py
+++ b/funnel/models/project.py
@@ -429,7 +429,6 @@ def permissions(self, user, inherited=None):
'edit-participant',
'view-participant',
'new-participant',
- 'new_labelset'
])
if self.review_team and user in self.review_team.users:
perms.update([
@@ -490,17 +489,6 @@ def roles_for(self, actor=None, anchors=()):
roles.update(self.profile.roles_for(actor, anchors))
return roles
- @with_roles(call={'admin'})
- def assign_labelset(self, labelset):
- """
- This function takes a Labelset object and links the project with it.
- This function requires role control. Hence this function must be called
- via ``current_access()``.
-
- :param labelset: A Labelset instance
- """
- self.labelsets.append(labelset)
-
Profile.listed_projects = db.relationship(
Project, lazy='dynamic',
diff --git a/funnel/models/proposal.py b/funnel/models/proposal.py
index a0f9fbf50..4e2daf2a9 100644
--- a/funnel/models/proposal.py
+++ b/funnel/models/proposal.py
@@ -371,12 +371,12 @@ def assign_label(self, label):
if label.parent_label is not None:
existing_labels = set(label.parent_label.children).intersection(set(self.labels))
if existing_labels:
- # the labelset is in radio mode and one of it's labels are
+ # the parent label is in radio mode and one of it's labels are
# already assigned to this proposal. We need to
# remove the older label and assign given label.
for elabel in existing_labels:
self.labels.remove(elabel)
- # labelset is not in radio mode, so we can assign label to proposal
+ # label is not in radio mode, so we can assign label to proposal
self.labels.append(label)
diff --git a/funnel/templates/macros.html.jinja2 b/funnel/templates/macros.html.jinja2
index 491a95a4f..306f39814 100644
--- a/funnel/templates/macros.html.jinja2
+++ b/funnel/templates/macros.html.jinja2
@@ -179,7 +179,7 @@
{% trans %}Edit schedule{% endtrans %}
- {% trans %}Manage labels{% endtrans %}
+ {% trans %}Manage labels{% endtrans %}
{% trans %}Manage venues{% endtrans %}
diff --git a/funnel/templates/proposal.html.jinja2 b/funnel/templates/proposal.html.jinja2
index 68c892370..2310df89e 100644
--- a/funnel/templates/proposal.html.jinja2
+++ b/funnel/templates/proposal.html.jinja2
@@ -100,7 +100,7 @@
{%- for label in proposal.labels %}
- {{ label.labelset.title }}: {{ label.title }}
+ {{ label.parent_label.title }}: {{ label.title }}
{%- endfor %}
diff --git a/funnel/views/label.py b/funnel/views/label.py
index 07d95eaa5..c61cd02ca 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -7,7 +7,7 @@
from .. import app, funnelapp, lastuser
from ..models import Label, db, Project, Profile
-from ..forms import LabelsetForm, LabelForm
+from ..forms import LabelForm
from .mixins import ProjectViewMixin
from .decorators import legacy_redirect
@@ -20,12 +20,12 @@ class ProjectLabelView(ProjectViewMixin, UrlForView, ModelView):
@render_with('labels.html.jinja2')
@lastuser.requires_login
@requires_permission('edit_project')
- def labelsets(self):
- return dict(project=self.obj, labelsets=self.obj.labelsets)
+ def labels(self):
+ return dict(project=self.obj, labels=self.obj.labels)
@route('new', methods=['GET', 'POST'])
@lastuser.requires_login
- @requires_permission('new_labelset')
+ @requires_permission('admin')
def new_label(self):
pass
@@ -64,7 +64,7 @@ def edit(self):
form.populate_obj(self.obj)
db.session.commit()
flash(_("Your label has been edited"), 'info')
- return redirect(self.obj.project.url_for('labelsets'), code=303)
+ return redirect(self.obj.project.url_for('labels'), code=303)
return render_form(form=form, title=_("Edit label"), submit=_("Save changes"))
From a083c63959d7389b68e008c07552f5aac2e6ac3a Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Tue, 16 Apr 2019 18:51:27 +0530
Subject: [PATCH 037/125] added missing project FK in migration
---
funnel/forms/proposal.py | 8 ++++----
funnel/templates/labels.html.jinja2 | 4 ++--
migrations/versions/0b25df40d307_add_labels.py | 2 ++
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/funnel/forms/proposal.py b/funnel/forms/proposal.py
index 052dd2370..73a9d9117 100644
--- a/funnel/forms/proposal.py
+++ b/funnel/forms/proposal.py
@@ -15,10 +15,10 @@ class TransferProposal(forms.Form):
class ProposalLabelsForm(forms.Form):
- labels = QuerySelectMultipleField(__("Labels"), get_label='title', get_pk=lambda l: l.name)
+ labels = forms.SelectMultipleField(__("Labels"))
def set_queries(self):
- self.labels.query = self.edit_parent.labels
+ self.labels.choices = [(l.name, l.title) for l in self.edit_parent.labels]
class ProposalForm(forms.Form):
@@ -56,7 +56,7 @@ class ProposalForm(forms.Form):
location = forms.StringField(__("Your location"), validators=[forms.validators.DataRequired(), forms.validators.Length(max=80)],
description=__("Your location, to help plan for your travel if required"))
- labels = QuerySelectMultipleField(__("Labels"), get_label='title', get_pk=lambda l: l.name)
+ labels = forms.SelectMultipleField(__("Labels"))
def __init__(self, *args, **kwargs):
super(ProposalForm, self).__init__(*args, **kwargs)
@@ -71,7 +71,7 @@ def __init__(self, *args, **kwargs):
self.description.description = project.proposal_part_b.get('hint')
def set_queries(self):
- self.labels.query = self.edit_parent.labels
+ self.labels.choices = [(l.name, l.title) for l in self.edit_parent.labels]
class ProposalTransitionForm(forms.Form):
diff --git a/funnel/templates/labels.html.jinja2 b/funnel/templates/labels.html.jinja2
index 6adde5b80..74fede4e9 100644
--- a/funnel/templates/labels.html.jinja2
+++ b/funnel/templates/labels.html.jinja2
@@ -39,7 +39,7 @@
{%- endfor %}
-
+
@@ -51,7 +51,7 @@
diff --git a/migrations/versions/0b25df40d307_add_labels.py b/migrations/versions/0b25df40d307_add_labels.py
index f4c6b3dbe..73fa9ff03 100644
--- a/migrations/versions/0b25df40d307_add_labels.py
+++ b/migrations/versions/0b25df40d307_add_labels.py
@@ -18,6 +18,7 @@ def upgrade():
op.create_table('label',
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
+ sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('parent_label_id', sa.Integer(), nullable=False),
sa.Column('seq', sa.Integer(), nullable=False),
sa.Column('description', sa.UnicodeText(), nullable=False),
@@ -30,6 +31,7 @@ def upgrade():
sa.Column('title', sa.Unicode(length=250), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['parent_label_id'], ['label.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('parent_label_id', 'name')
)
From a2cb4b10270e1bd535ba8a74dbd12badaf05b6b6 Mon Sep 17 00:00:00 2001
From: Vidya Ramakrishnan
Date: Wed, 17 Apr 2019 12:03:58 +0530
Subject: [PATCH 038/125] Label form(WIP)
---
funnel/templates/labels.html.jinja2 | 19 +++--
funnel/templates/labels_form.html.jinja2 | 95 ++++++++++++++++++++++++
funnel/views/label.py | 8 +-
3 files changed, 110 insertions(+), 12 deletions(-)
create mode 100644 funnel/templates/labels_form.html.jinja2
diff --git a/funnel/templates/labels.html.jinja2 b/funnel/templates/labels.html.jinja2
index 74fede4e9..e3e5239f4 100644
--- a/funnel/templates/labels.html.jinja2
+++ b/funnel/templates/labels.html.jinja2
@@ -8,20 +8,20 @@
{% block contentwrapper %}
- {%- for labelset in labelsets %}
+ {%- for label in labels %}
- {%- for label in labelset.labels %}
+ {%- for label in labels.children %}
-
{{ label.title }}
- edit
+ edit
delete
@@ -32,14 +32,14 @@
{%- endfor %}
-
+
@@ -47,8 +47,7 @@
-
Create a new labelset
-
+ Create a new label
{%- endif %}
+ {%- if subform %}
{%- endif %}
{%- if not subform %}
- {{ renderfield(form.required) }}
+ {{ renderfield(form.required, css_class="mui--hide js-required-field") }}
{%- for error in form.required.errors %}
{%- endfor %}
@@ -96,7 +88,8 @@
event.preventDefault();
$.modal.close();
$('#inner-forms').append($('#child-form').html());
- $('.subtitle').removeClass('mui--hide');
+ $('.subtitle, .js-required-field').removeClass('mui--hide');
+ $('.js-required-field input').click();
});
});
From 369c09927aae6229df56ef06653c7e7a59cfa4b5 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Wed, 17 Apr 2019 18:18:27 +0530
Subject: [PATCH 041/125] added sublabel form
---
funnel/forms/label.py | 11 +++++++++++
funnel/models/proposal.py | 14 +++++++-------
funnel/views/label.py | 2 +-
3 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/funnel/forms/label.py b/funnel/forms/label.py
index 190bbe1b5..81794a9c5 100644
--- a/funnel/forms/label.py
+++ b/funnel/forms/label.py
@@ -25,3 +25,14 @@ class LabelForm(forms.Form):
def validate_bgcolor(self, field):
if not valid_color_re.match(field.data):
raise forms.ValidationError("Please enter a valid color code")
+
+
+class SublabelForm(forms.Form):
+ title = forms.StringField(__("Name"), description=__("Name of the label"),
+ validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
+ bgcolor = forms.StringField(__("Label Color"),
+ validators=[forms.validators.DataRequired(), forms.validators.Length(max=6)],
+ description=__("RGB Color for the label. Enter without the '#'. E.g. CCCCCC."), default=u"CCCCCC")
+ icon_emoji = forms.StringField(__("Icon/Emoji"),
+ validators=[forms.validators.Length(max=2)],
+ description=__("Emoji to be used for this label for space constrained UI"))
diff --git a/funnel/models/proposal.py b/funnel/models/proposal.py
index 4e2daf2a9..413f24a6c 100644
--- a/funnel/models/proposal.py
+++ b/funnel/models/proposal.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
-from flask import abort
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.hybrid import hybrid_property
+from werkzeug.utils import cached_property
from . import db, TimestampMixin, UuidMixin, BaseScopedIdNameMixin, MarkdownColumn, JsonDict, CoordinatesMixin, UrlType
from .user import User
from .project import Project
@@ -9,10 +11,6 @@
from coaster.utils import LabeledEnum
from coaster.sqlalchemy import SqlSplitIdComparator, StateManager, with_roles
from baseframe import __
-from sqlalchemy.ext.hybrid import hybrid_property
-from flask import request
-from pytz import timezone, utc, UnknownTimeZoneError
-from werkzeug.utils import cached_property
from ..util import geonameid_from_location
__all__ = ['PROPOSAL_STATE', 'Proposal', 'ProposalRedirect']
@@ -141,6 +139,9 @@ class Proposal(UuidMixin, BaseScopedIdNameMixin, CoordinatesMixin, db.Model):
# Additional form data
data = db.Column(JsonDict, nullable=False, server_default='{}')
+ # for managing relationship with labels easily
+ labels = association_proxy('labels', 'name')
+
__table_args__ = (db.UniqueConstraint('project_id', 'url_id'),)
# XXX: The following two may overlap. Reconsider whether both are needed
@@ -333,8 +334,7 @@ def permissions(self, user, inherited=None):
perms.update([
'vote_proposal',
'new_comment',
- 'vote_comment',
- 'edit-proposal'
+ 'vote_comment'
])
if user == self.owner:
perms.update([
diff --git a/funnel/views/label.py b/funnel/views/label.py
index b52e8d2eb..428727b71 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -27,7 +27,7 @@ def labels(self):
@lastuser.requires_login
@requires_permission('admin')
def new_label(self):
- form = LabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
+ form = LabelForm(model=Label, parent=self.obj.parent)
# return jsonify(
# title="Add label",
# form=render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj))
From 5cd4341d5f656dd8a185a0d82ac7ec172dcdff13 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Wed, 17 Apr 2019 19:10:07 +0530
Subject: [PATCH 042/125] added sublabelform
---
funnel/forms/label.py | 5 +--
funnel/templates/labels.html.jinja2 | 36 +++++++++---------
funnel/views/label.py | 38 ++++++++++++++++---
.../versions/0b25df40d307_add_labels.py | 3 +-
4 files changed, 54 insertions(+), 28 deletions(-)
diff --git a/funnel/forms/label.py b/funnel/forms/label.py
index 8aa624c23..aa3a358b0 100644
--- a/funnel/forms/label.py
+++ b/funnel/forms/label.py
@@ -4,7 +4,7 @@
import baseframe.forms as forms
from .project import valid_color_re
-__all__ = ['LabelForm']
+__all__ = ['LabelForm', 'SublabelForm']
class LabelForm(forms.Form):
@@ -26,9 +26,6 @@ def validate_bgcolor(self, field):
class SublabelForm(forms.Form):
title = forms.StringField(__("Name"), description=__("Name of the label"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
- bgcolor = forms.StringField(__("Label Color"),
- validators=[forms.validators.DataRequired(), forms.validators.Length(max=6)],
- description=__("RGB Color for the label. Enter without the '#'. E.g. CCCCCC."), default=u"CCCCCC")
icon_emoji = forms.StringField(__("Icon/Emoji"),
validators=[forms.validators.Length(max=2)],
description=__("Emoji to be used for this label for space constrained UI"))
diff --git a/funnel/templates/labels.html.jinja2 b/funnel/templates/labels.html.jinja2
index e3e5239f4..2fa86426c 100644
--- a/funnel/templates/labels.html.jinja2
+++ b/funnel/templates/labels.html.jinja2
@@ -15,25 +15,27 @@
{{ label.title }}
{{ label.description }}
-
-
- {%- for label in labels.children %}
- -
- {{ label.title }}
-
- edit
- delete
-
-
- {% else %}
- - {% trans %}(No labels){% endtrans %}
- {%- endfor %}
-
-
-
+ {% if label.is_parent %}
+
+
+ {%- for sublabel in label.children %}
+ -
+ {{ sublabel.title }}
+
+ edit
+ delete
+
+
+ {% else %}
+ - {% trans %}(No labels){% endtrans %}
+ {%- endfor %}
+
+
+
+ {% endif %}
diff --git a/funnel/views/label.py b/funnel/views/label.py
index 428727b71..baf6e1e4f 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
-from flask import flash, redirect, g, render_template
+from flask import flash, redirect, g, render_template, request
from coaster.views import render_with, requires_permission, route, UrlForView, ModelView
from baseframe import _
from baseframe.forms import render_delete_sqla, render_form
from .. import app, funnelapp, lastuser
from ..models import Label, db, Project, Profile
-from ..forms import LabelForm
+from ..forms import LabelForm, SublabelForm
from .mixins import ProjectViewMixin
from .decorators import legacy_redirect
@@ -28,9 +28,37 @@ def labels(self):
@requires_permission('admin')
def new_label(self):
form = LabelForm(model=Label, parent=self.obj.parent)
- # return jsonify(
- # title="Add label",
- # form=render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj))
+ if form.validate_on_submit():
+ # This form can send one or multiple values for title and icon_emoji.
+ # If the label doesn't have any sublabel, one value is sent for each list,
+ # and those values are also available at `form.data`.
+ # But in case there are sublabels, the sublabel values are in the list
+ # in the order they appeared on the create form.
+ titlelist = request.values.getlist('title')
+ emojilist = request.values.getlist('icon_emoji')
+ # first values of both lists belong to the parent label
+ titlelist.pop(0)
+ emojilist.pop(0)
+
+ label = Label(project=self.obj)
+ form.populate_obj(label)
+ self.obj.labels.append(label)
+ db.session.add(label)
+
+ for idx, title in enumerate(titlelist):
+ subform = SublabelForm(title=titlelist[idx], icon_emoji=emojilist[idx])
+ if not subform.validate():
+ flash(_("Error with a sublabel: {}").format(subform.errors.pop()), category='error')
+ return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
+ else:
+ subl = Label(project=self.obj)
+ subform.populate_obj(subl)
+ db.session.add(subl)
+ label.children.append(subl)
+
+ import ipdb; ipdb.set_trace()
+ db.session.commit()
+ return redirect(self.obj.url_for('labels'), code=303)
return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
diff --git a/migrations/versions/0b25df40d307_add_labels.py b/migrations/versions/0b25df40d307_add_labels.py
index 73fa9ff03..95b531162 100644
--- a/migrations/versions/0b25df40d307_add_labels.py
+++ b/migrations/versions/0b25df40d307_add_labels.py
@@ -19,10 +19,9 @@ def upgrade():
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
- sa.Column('parent_label_id', sa.Integer(), nullable=False),
+ sa.Column('parent_label_id', sa.Integer(), nullable=True),
sa.Column('seq', sa.Integer(), nullable=False),
sa.Column('description', sa.UnicodeText(), nullable=False),
- sa.Column('bgcolor', sa.Unicode(length=6), nullable=False),
sa.Column('icon_emoji', sa.UnicodeText(), nullable=True),
sa.Column('archived', sa.Boolean(), nullable=False),
sa.Column('restricted', sa.Boolean(), nullable=False),
From 95b102873c052970ed0a0b7eee99f8e12946ee2c Mon Sep 17 00:00:00 2001
From: Vidya Ramakrishnan
Date: Thu, 18 Apr 2019 01:16:22 +0530
Subject: [PATCH 043/125] Add emoji picker
---
funnel/__init__.py | 13 ++++
funnel/forms/label.py | 7 +-
funnel/static/css/app.css | 40 ++++++++---
funnel/static/sass/_form.scss | 50 ++++++++++----
funnel/templates/labels_form.html.jinja2 | 84 ++++++++++--------------
5 files changed, 122 insertions(+), 72 deletions(-)
diff --git a/funnel/__init__.py b/funnel/__init__.py
index 1cff04e73..70ac06b0a 100644
--- a/funnel/__init__.py
+++ b/funnel/__init__.py
@@ -100,6 +100,13 @@
app.assets.register('css_leaflet',
Bundle(assets.require('leaflet.css', 'leaflet-search.css'),
output='css/leaflet.packed.css', filters='cssmin'))
+app.assets.register('js_emojionearea',
+ Bundle(assets.require('!jquery.js', 'emojionearea.js'),
+ output='js/emojionearea.packed.js', filters='uglipyjs'))
+app.assets.register('css_emojionearea',
+ Bundle(assets.require('emojionearea.css'),
+ output='css/emojionearea.packed.css', filters='cssmin'))
+
baseframe.init_app(funnelapp, requires=['funnel'], ext_requires=[
'pygments', 'toastr', 'baseframe-mui'], theme='mui')
@@ -133,6 +140,12 @@
funnelapp.assets.register('css_leaflet',
Bundle(assets.require('leaflet.css', 'leaflet-search.css'),
output='css/leaflet.packed.css', filters='cssmin'))
+funnelapp.assets.register('js_emojionearea',
+ Bundle(assets.require('!jquery.js', 'emojionearea.js'),
+ output='js/emojionearea.packed.js', filters='uglipyjs'))
+funnelapp.assets.register('css_emojionearea',
+ Bundle(assets.require('emojionearea.css'),
+ output='css/emojionearea.packed.css', filters='cssmin'))
# FIXME: Hack for external build system generating relative /static URLs.
# Fix this by generating absolute URLs to the static subdomain during build.
diff --git a/funnel/forms/label.py b/funnel/forms/label.py
index 8f7280989..afe6713ee 100644
--- a/funnel/forms/label.py
+++ b/funnel/forms/label.py
@@ -8,11 +8,10 @@
class LabelForm(forms.Form):
- title = forms.StringField(__("Name"), description=__("Name of the label"),
+ title = forms.StringField(__("Title"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
- icon_emoji = forms.StringField(__("Icon/Emoji"),
- validators=[forms.validators.Length(max=2)],
- description=__("Emoji to be used for this label for space constrained UI"))
+ icon_emoji = forms.StringField(__(""),
+ validators=[forms.validators.Length(max=2)])
required = forms.BooleanField(__("Required"), default=False,
description=__("When required is set, this label must be set for a proposal (e.g. Proposal Type)."))
restricted = forms.BooleanField(__("Restricted"), default=False,
diff --git a/funnel/static/css/app.css b/funnel/static/css/app.css
index 8165465df..2f58eea6c 100644
--- a/funnel/static/css/app.css
+++ b/funnel/static/css/app.css
@@ -411,16 +411,40 @@ html.touch .collapsible__body--disable {
vertical-align: middle;
}
-.js-modal-inner .card-wrapper {
- width: 100%;
- padding: 0;
+.label-title {
+ width: calc(100% - 90px);
+ display: inline-block;
}
-.js-modal-inner .card-wrapper .card {
- box-shadow: none;
- margin-bottom: 0;
+
+.mui-form__fields.emojipicker {
+ float: left;
+ width: 70px;
+ height: 48px;
+ margin-right: 20px;
}
-.js-modal-inner .card-wrapper .card .card__body {
- padding: 0;
+.mui-form__fields.emojipicker .emojionearea-editor {
+ height: 48px;
+ min-height: 20px;
+ overflow: hidden;
+ white-space: nowrap;
+ position: absolute;
+ top: 0;
+ left: 12px;
+ right: 24px;
+ padding: 6px 0;
+}
+.mui-form__fields.emojipicker .emojionearea-button {
+ top: 10px;
+}
+.mui-form__fields.emojipicker .emojionearea-editor {
+ left: 6px !important;
+}
+.mui-form__fields.emojipicker .emojionearea-button .emojionearea-button-open {
+ right: 0;
+}
+
+.label-form-modal {
+ overflow: visible;
}
.clickable-card {
diff --git a/funnel/static/sass/_form.scss b/funnel/static/sass/_form.scss
index 388ab912d..8a7f5d9a7 100644
--- a/funnel/static/sass/_form.scss
+++ b/funnel/static/sass/_form.scss
@@ -21,16 +21,42 @@
vertical-align: middle;
}
-.js-modal-inner {
- .card-wrapper {
- width: 100%;
- padding: 0;
- .card {
- box-shadow: none;
- margin-bottom: 0;
- .card__body {
- padding: 0;
- }
- }
- }
+.label-title {
+ width: calc(100% - 90px);
+ display: inline-block;
+}
+
+.mui-form__fields.emojipicker {
+ float: left;
+ width: 70px;
+ height: 48px;
+ margin-right: 20px;
+
+ .emojionearea-editor {
+ height: 48px;
+ min-height: 20px;
+ overflow: hidden;
+ white-space: nowrap;
+ position: absolute;
+ top: 0;
+ left: 12px;
+ right: 24px;
+ padding: 6px 0;
+ }
+
+ .emojionearea-button {
+ top: 10px;
+ }
+
+ .emojionearea-editor {
+ left: 6px !important;
+ }
+
+ .emojionearea-button .emojionearea-button-open {
+ right: 0;
+ }
+}
+
+.label-form-modal {
+ overflow: visible;
}
\ No newline at end of file
diff --git a/funnel/templates/labels_form.html.jinja2 b/funnel/templates/labels_form.html.jinja2
index a36929204..762e1da9b 100644
--- a/funnel/templates/labels_form.html.jinja2
+++ b/funnel/templates/labels_form.html.jinja2
@@ -3,29 +3,25 @@
{% block title %}{{ title }}{% endblock %}
{% block pageheaders %}
+ {% assets "css_emojionearea" -%}
+
+ {%- endassets -%}
{% endblock %}
{% macro labelformfields(forms, subform=false) %}
- {%- if subform %}{%- endif %}
- {{ renderfield(form.title, autofocus=true) }}
- {%- for error in form.title.errors %}
+ {%- if subform %}
+
+
+ {%- endif %}
+ {{ renderfield(form.icon_emoji, css_class="emojipicker") }}
+ {%- for error in form.icon_emoji.errors %}
{%- endfor %}
- {{ renderfield(form.icon_emoji) }}
- {%- for error in form.icon_emoji.errors %}
+ {{ renderfield(form.title, autofocus=true, css_class="label-title") }}
+ {%- for error in form.title.errors %}
{%- endfor %}
- {%- if subform %}
{%- endif %}
- {%- if not subform %}
- {{ renderfield(form.required, css_class="mui--hide js-required-field") }}
- {%- for error in form.required.errors %}
-
- {%- endfor %}
- {{ renderfield(form.restricted) }}
- {%- for error in form.restricted.errors %}
-
- {%- endfor %}
- {%- endif %}
+ {%- if subform %}
{%- endif %}
{% endmacro %}
{% block content %}
@@ -41,12 +37,16 @@
-
{% endblock %}
{% block footerscripts %}
+ {% assets "js_emojionearea" -%}
+
+ {%- endassets -%}
From cb24ad836466780120440d1a2d5a849cd801b61b Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Thu, 18 Apr 2019 11:14:01 +0530
Subject: [PATCH 044/125] saving sublabels with emojis
---
funnel/templates/labels.html.jinja2 | 2 +-
funnel/templates/labels_form.html.jinja2 | 2 +-
funnel/views/label.py | 14 ++++++++++----
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/funnel/templates/labels.html.jinja2 b/funnel/templates/labels.html.jinja2
index 2fa86426c..736de67ee 100644
--- a/funnel/templates/labels.html.jinja2
+++ b/funnel/templates/labels.html.jinja2
@@ -23,7 +23,7 @@
{{ sublabel.title }}
edit
- delete
+ delete
{% else %}
diff --git a/funnel/templates/labels_form.html.jinja2 b/funnel/templates/labels_form.html.jinja2
index 762e1da9b..7fcc7ce5e 100644
--- a/funnel/templates/labels_form.html.jinja2
+++ b/funnel/templates/labels_form.html.jinja2
@@ -38,7 +38,7 @@
{{ form.hidden_tag() }}
{{ labelformfields(form) }}
-
+
{{ renderfield(form.required, css_class="mui--hide js-required-field") }}
{%- for error in form.required.errors %}
diff --git a/funnel/views/label.py b/funnel/views/label.py
index baf6e1e4f..f7c71c866 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from flask import flash, redirect, g, render_template, request
+from werkzeug.datastructures import MultiDict
from coaster.views import render_with, requires_permission, route, UrlForView, ModelView
from baseframe import _
from baseframe.forms import render_delete_sqla, render_form
@@ -40,23 +41,28 @@ def new_label(self):
titlelist.pop(0)
emojilist.pop(0)
- label = Label(project=self.obj)
- form.populate_obj(label)
+ label = Label(title=form.data.get('title'), icon_emoji=form.data.get('icon_emoji'), project=self.obj)
+ label.restricted = form.data.get('restricted')
+ label.make_name()
self.obj.labels.append(label)
db.session.add(label)
for idx, title in enumerate(titlelist):
- subform = SublabelForm(title=titlelist[idx], icon_emoji=emojilist[idx])
+ subform = SublabelForm(MultiDict({
+ 'csrf_token': form.csrf_token.data, 'title': titlelist[idx],
+ 'icon_emoji': emojilist[idx]
+ }))
+
if not subform.validate():
flash(_("Error with a sublabel: {}").format(subform.errors.pop()), category='error')
return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
else:
subl = Label(project=self.obj)
subform.populate_obj(subl)
+ subl.make_name()
db.session.add(subl)
label.children.append(subl)
- import ipdb; ipdb.set_trace()
db.session.commit()
return redirect(self.obj.url_for('labels'), code=303)
return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
From 6d83dfdb836de48cfb35363cef2f440383a862f8 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Thu, 18 Apr 2019 12:31:58 +0530
Subject: [PATCH 045/125] removed length validator from emoji field
---
funnel/forms/label.py | 5 ++---
funnel/views/label.py | 13 ++++++++++++-
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/funnel/forms/label.py b/funnel/forms/label.py
index 9e24f0987..8bbf370eb 100644
--- a/funnel/forms/label.py
+++ b/funnel/forms/label.py
@@ -10,8 +10,8 @@
class LabelForm(forms.Form):
title = forms.StringField(__("Title"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
- icon_emoji = forms.StringField(__(""),
- validators=[forms.validators.Length(max=2)])
+ icon_emoji = forms.StringField(__("Icon/Emoji"),
+ description=__("Emoji to be used for this label for space constrained UI"))
required = forms.BooleanField(__("Required"), default=False,
description=__("When required is set, this label must be set for a proposal (e.g. Proposal Type)."))
restricted = forms.BooleanField(__("Restricted"), default=False,
@@ -26,5 +26,4 @@ class SublabelForm(forms.Form):
title = forms.StringField(__("Name"), description=__("Name of the label"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=250)])
icon_emoji = forms.StringField(__("Icon/Emoji"),
- validators=[forms.validators.Length(max=2)],
description=__("Emoji to be used for this label for space constrained UI"))
diff --git a/funnel/views/label.py b/funnel/views/label.py
index f7c71c866..73afc06dc 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -97,7 +97,12 @@ def loader(self, profile, project, label):
@lastuser.requires_login
@requires_permission('edit_project')
def edit(self):
- form = LabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
+ if self.obj.parent_label:
+ # It's a sublabel
+ form = SublabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
+ else:
+ form = LabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
+
if form.validate_on_submit():
form.populate_obj(self.obj)
db.session.commit()
@@ -105,6 +110,12 @@ def edit(self):
return redirect(self.obj.project.url_for('labels'), code=303)
return render_form(form=form, title=_("Edit label"), submit=_("Save changes"))
+ @route('archive', methods=['POST'])
+ @lastuser.requires_login
+ @requires_permission('admin')
+ def archive(self):
+ return redirect(self.obj.project.url_for('labels'), code=303)
+
@route('//labels/', subdomain='')
class FunnelLabelView(LabelView):
From 580dce1867f9004de545056a92e13f18d6bcfce0 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Thu, 18 Apr 2019 12:33:18 +0530
Subject: [PATCH 046/125] sending an empty sublabel form to template
---
funnel/views/label.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/funnel/views/label.py b/funnel/views/label.py
index 73afc06dc..b5e11a374 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -29,6 +29,7 @@ def labels(self):
@requires_permission('admin')
def new_label(self):
form = LabelForm(model=Label, parent=self.obj.parent)
+ emptysubform = SublabelForm(MultiDict({}))
if form.validate_on_submit():
# This form can send one or multiple values for title and icon_emoji.
# If the label doesn't have any sublabel, one value is sent for each list,
@@ -65,7 +66,7 @@ def new_label(self):
db.session.commit()
return redirect(self.obj.url_for('labels'), code=303)
- return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
+ return render_template('labels_form.html.jinja2', title="Add label", form=form, subform=emptysubform, project=self.obj)
@route('//labels', subdomain='')
From d5e5ecc539ccdfaeeaeb948b7bc5f183bb6bcd74 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Thu, 18 Apr 2019 18:08:12 +0530
Subject: [PATCH 047/125] modified how choices are formatted
---
funnel/forms/proposal.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/funnel/forms/proposal.py b/funnel/forms/proposal.py
index 73a9d9117..fcfc3c5b4 100644
--- a/funnel/forms/proposal.py
+++ b/funnel/forms/proposal.py
@@ -18,7 +18,14 @@ class ProposalLabelsForm(forms.Form):
labels = forms.SelectMultipleField(__("Labels"))
def set_queries(self):
- self.labels.choices = [(l.name, l.title) for l in self.edit_parent.labels]
+ choices = []
+ for l in self.edit_parent.labels:
+ if l.is_parent:
+ children = [(sl.name, sl.title) for sl in l.children]
+ choices.append((l.title, children))
+ else:
+ choices.append((l.name, l.title))
+ self.labels.choices = choices
class ProposalForm(forms.Form):
@@ -71,7 +78,14 @@ def __init__(self, *args, **kwargs):
self.description.description = project.proposal_part_b.get('hint')
def set_queries(self):
- self.labels.choices = [(l.name, l.title) for l in self.edit_parent.labels]
+ choices = []
+ for l in self.edit_parent.labels:
+ if l.is_parent:
+ children = [(sl.name, sl.title) for sl in l.children]
+ choices.append((l.title, children))
+ else:
+ choices.append((l.name, l.title))
+ self.labels.choices = choices
class ProposalTransitionForm(forms.Form):
From ecb8b002abf7688088aae4f071c61453562834cb Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Fri, 19 Apr 2019 13:10:47 +0530
Subject: [PATCH 048/125] using labels_form template for label edit
---
funnel/views/label.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/funnel/views/label.py b/funnel/views/label.py
index b5e11a374..0b20f8a10 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -26,6 +26,7 @@ def labels(self):
@route('new', methods=['GET', 'POST'])
@lastuser.requires_login
+ @render_with('labels_form.html.jinja2')
@requires_permission('admin')
def new_label(self):
form = LabelForm(model=Label, parent=self.obj.parent)
@@ -56,7 +57,7 @@ def new_label(self):
if not subform.validate():
flash(_("Error with a sublabel: {}").format(subform.errors.pop()), category='error')
- return render_template('labels_form.html.jinja2', title="Add label", form=form, project=self.obj)
+ return dict(title="Add label", form=form, project=self.obj)
else:
subl = Label(project=self.obj)
subform.populate_obj(subl)
@@ -66,7 +67,7 @@ def new_label(self):
db.session.commit()
return redirect(self.obj.url_for('labels'), code=303)
- return render_template('labels_form.html.jinja2', title="Add label", form=form, subform=emptysubform, project=self.obj)
+ return dict(title="Add label", form=form, subform=emptysubform, project=self.obj)
@route('//labels', subdomain='')
@@ -96,6 +97,7 @@ def loader(self, profile, project, label):
@route('edit', methods=['GET', 'POST'])
@lastuser.requires_login
+ @render_with('labels_form.html.jinja2')
@requires_permission('edit_project')
def edit(self):
if self.obj.parent_label:
@@ -103,13 +105,14 @@ def edit(self):
form = SublabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
else:
form = LabelForm(obj=self.obj, model=Label, parent=self.obj.parent)
+ emptysubform = SublabelForm(MultiDict({}))
if form.validate_on_submit():
form.populate_obj(self.obj)
db.session.commit()
flash(_("Your label has been edited"), 'info')
return redirect(self.obj.project.url_for('labels'), code=303)
- return render_form(form=form, title=_("Edit label"), submit=_("Save changes"))
+ return dict(title="Add label", form=form, subform=emptysubform, project=self.obj.project)
@route('archive', methods=['POST'])
@lastuser.requires_login
From 2de33375c20a2e8aa094ac9e51bd832759b94116 Mon Sep 17 00:00:00 2001
From: Bibhas
Date: Fri, 19 Apr 2019 13:29:44 +0530
Subject: [PATCH 049/125] sending sublabels to label edit form
---
funnel/templates/labels_form.html.jinja2 | 18 ++++++++++++------
funnel/views/label.py | 12 ++++++++----
2 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/funnel/templates/labels_form.html.jinja2 b/funnel/templates/labels_form.html.jinja2
index 7fcc7ce5e..b5f1d2022 100644
--- a/funnel/templates/labels_form.html.jinja2
+++ b/funnel/templates/labels_form.html.jinja2
@@ -8,17 +8,17 @@
{%- endassets -%}
{% endblock %}
-{% macro labelformfields(forms, subform=false) %}
+{% macro labelformfields(lform, subform=false) %}
{%- if subform %}
{%- endif %}
- {{ renderfield(form.icon_emoji, css_class="emojipicker") }}
- {%- for error in form.icon_emoji.errors %}
+ {{ renderfield(lform.icon_emoji, css_class="emojipicker") }}
+ {%- for error in lform.icon_emoji.errors %}
{%- endfor %}
- {{ renderfield(form.title, autofocus=true, css_class="label-title") }}
- {%- for error in form.title.errors %}
+ {{ renderfield(lform.title, autofocus=true, css_class="label-title") }}
+ {%- for error in lform.title.errors %}
{%- endfor %}
{%- if subform %}
{%- endif %}
@@ -37,7 +37,13 @@