From a16a62197d3771d48dffb60e65e81cd10c7392fe Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Mon, 12 Feb 2024 23:16:09 +0000 Subject: [PATCH 01/15] wip Fix #1281 As a shop owner I can assign managers to plan(s) --- ...83d_add_association_table_plan_to_users.py | 47 +++++++++++ subscribie/blueprints/admin/__init__.py | 19 +++++ .../admin/assign_managers_to_plan.html | 84 +++++++++++++++++++ .../admin/templates/admin/dashboard.html | 1 + subscribie/models.py | 22 ++++- 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/d80effffd83d_add_association_table_plan_to_users.py create mode 100644 subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html diff --git a/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py b/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py new file mode 100644 index 00000000..988db015 --- /dev/null +++ b/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py @@ -0,0 +1,47 @@ +"""add association_table_plan_to_users + +Revision ID: d80effffd83d +Revises: 48074e6225c6 +Create Date: 2024-02-12 12:24:44.482877 + +Why? + +Some shop owners want/need to assign managers (users) to +plans. For example large clubs or membership organisations which +assign a 'manager' to one or more plans. + +The plan_user_associations table begins to make possible the +assignment of Users to Plans. Recall that Users (see class User +in models.py) is a shop owner (admin) which may login to the +Subscribie application. + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d80effffd83d" +down_revision = "48074e6225c6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "plan_user_associations", + sa.Column("plan_uuid", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["plan_uuid"], + ["plan.uuid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + ) + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 23f753b8..7be91f5a 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -788,6 +788,25 @@ def delete_plan_by_uuid(uuid): return render_template("admin/delete_plan_choose.html", plans=plans) +@admin.route("assign-managers-to-plan") +@login_required +def assign_managers_to_plan(): + """ + assign users (managers) to a plan. + + Some shop owners want/need to assign managers (users) to + plans. For example large clubs or membership organisations which + assign a 'manager' to one or more plans. + + The plan_user_associations table begins to make possible the + assignment of Users to Plans. Recall that Users (see class User + in models.py) is a shop owner (admin) which may login to the + Subscribie application. + """ + users = User.query.all() + return render_template("admin/assign_managers_to_plan.html", users=users) + + @admin.route("/list-documents", methods=["get"]) @login_required def list_documents(): diff --git a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html new file mode 100644 index 00000000..16ecf55b --- /dev/null +++ b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html @@ -0,0 +1,84 @@ +{% extends "admin/layout.html" %} +{% block title %} {{ title }} {% endblock %} + +{% block body %} + +

Assign manager(s) to plans

+ +
+ +
+ +
+
+
+

Allow managers to quickly see plans they're responsible for

+
+

Some larger organisations like to assign people to 'manage' Subscribers who are subscribed to specific plans.
+ For example, if you're a sports club or large membership organisation you can assign specific staff to + become 'managers' of specific plans. This helps them see only the Subscribers they are responsible for.

+ +

+ Once you do so, when they login to their account, the visibility of the Subscribers they see + will be highlighted, and by default any Subscribers they are not a manager of given less Precedence, based + on the Plans they are assigned to as a manager.

+
+

+ Please note this is not a security feature- it does not stop managers being able to access + all plans, it simply makes it easier for a manager of specific plans to quickly see the Subscribers + who are subscribed to the plans they manage. +

+ +

Select a user to assign plan(s) to them.

+ +
+ + {% if confirm is sameas false %} +
+

Are you sure?

+ +
+ {% else %} + + + + + + + + + {% for user in users %} + + + + + {% endfor %} +
Title
+ {{ user.email}} + + + Assign Plan(s) + +
+ {% endif %} + +
+
+
+ +{% endblock body %} diff --git a/subscribie/blueprints/admin/templates/admin/dashboard.html b/subscribie/blueprints/admin/templates/admin/dashboard.html index c5f26a3b..225fa299 100644 --- a/subscribie/blueprints/admin/templates/admin/dashboard.html +++ b/subscribie/blueprints/admin/templates/admin/dashboard.html @@ -15,6 +15,7 @@

Manage My Shop

Checklist

+ Assign managers to plan

Make sure everything's in order. If tasks appear below, then you'll need to complete them to get the most out of your shop. diff --git a/subscribie/models.py b/subscribie/models.py index 697424c2..02c03c57 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -579,7 +579,16 @@ class Company(database.Model): database.Column("plan_id", database.Integer, ForeignKey("plan.id")), ) - +""" +Some shop owners want/need to assign managers (users) to +plans. For example large clubs or membership organisations which +assign a 'manager' to one or more plans. + +The plan_user_associations table begins to make possible the +assignment of Users to Plans. Recall that Users (see class User +in models.py) is a shop owner (admin) which may login to the +Subscribie application. +""" association_table_plan_to_price_lists = database.Table( "plan_price_list_associations", database.Column( @@ -595,6 +604,12 @@ class Company(database.Model): ), ) +association_table_plan_to_users = database.Table( + "plan_user_associations", + database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), + database.Column("user_id", database.String, ForeignKey("user.id")), +) + class INTERVAL_UNITS(Enum): DAILY = _("daily") @@ -656,6 +671,11 @@ class Plan(database.Model, HasArchived): price_lists = relationship( "PriceList", secondary=association_table_plan_to_price_lists ) + managers = relationship( + "User", + secondary=association_table_plan_to_users, + backref=database.backref("plans", lazy="dynamic"), + ) def getPrice(self, currency): """Returns a tuple of sell_price and interval_amount of the plan for From 321fd28e8f4c772493edb39eeec1812ac285260f Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Fri, 16 Feb 2024 22:59:33 +0000 Subject: [PATCH 02/15] wip #1281 assign user to plans see also #1305 --- ...452d3a80_add_catagory_user_associations.py | 36 +++++++++++ subscribie/blueprints/admin/__init__.py | 27 ++++++++ .../admin/assign_managers_to_plan.html | 62 +++++++------------ .../templates/admin/user_assign_plan.html | 61 ++++++++++++++++++ subscribie/models.py | 22 ++++--- 5 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 migrations/versions/9083452d3a80_add_catagory_user_associations.py create mode 100644 subscribie/blueprints/admin/templates/admin/user_assign_plan.html diff --git a/migrations/versions/9083452d3a80_add_catagory_user_associations.py b/migrations/versions/9083452d3a80_add_catagory_user_associations.py new file mode 100644 index 00000000..07387ceb --- /dev/null +++ b/migrations/versions/9083452d3a80_add_catagory_user_associations.py @@ -0,0 +1,36 @@ +"""add catagory_user_associations + +Revision ID: 9083452d3a80 +Revises: 48074e6225c6 +Create Date: 2024-02-16 21:38:11.302398 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9083452d3a80" +down_revision = "48074e6225c6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "catagory_user_associations", + sa.Column("category_uuid", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["category_uuid"], + ["category.uuid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + ) + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 7be91f5a..475b02c1 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -807,6 +807,33 @@ def assign_managers_to_plan(): return render_template("admin/assign_managers_to_plan.html", users=users) +@admin.route("/assign-managers-to-plans//assign-plan", methods=["GET", "POST"]) +@login_required +def user_assign_to_plans(user_id): + user = User.query.get(user_id) + plans = Plan.query.execution_options(include_archived=True) + + if request.method == "POST": + # Remove if not selected + for plan in plans: + if plan in user.plans: + user.plans.remove(plan) + + for plan_id in request.form.getlist("assign"): + plan = Plan.query.execution_options(include_archived=True).get(plan_id) + plan.managers.append(user) + + database.session.commit() + flash("User has been assigned the selected plan(s) as a manager of them") + return redirect(url_for("admin.assign_managers_to_plan")) + + return render_template( + "admin/user_assign_plan.html", + user=user, + plans=plans, + ) + + @admin.route("/list-documents", methods=["get"]) @login_required def list_documents(): diff --git a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html index 16ecf55b..5a5c226f 100644 --- a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html +++ b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html @@ -9,8 +9,7 @@

Assign manager(s) to plans

@@ -38,44 +37,29 @@

Allow managers to quickly see plans they're responsible for

- {% if confirm is sameas false %} -
-

Are you sure?

-
diff --git a/subscribie/blueprints/admin/templates/admin/user_assign_plan.html b/subscribie/blueprints/admin/templates/admin/user_assign_plan.html new file mode 100644 index 00000000..7a94f14b --- /dev/null +++ b/subscribie/blueprints/admin/templates/admin/user_assign_plan.html @@ -0,0 +1,61 @@ +{% extends "admin/layout.html" %} +{% block title %} {{ title }} {% endblock %} + +{% block body %} + +

Choice Group - Assign Plan

+ +
+ +
+ +
+
+
+ +

Assign Plan(s) to user: {{ user.email }}

+

Tick which plan(s) you want to assign the user to

+

Doing so will make these plans(s) and any associated subscriptions appear more prominently for them when they login.

+ +
+
Check all
+
Check none
+
+ {% for plan in plans %} + {% if plan.subscriptions | length > 0 %} +
+
+
+ +
+
+ {{ plan.archived }}| Subscriptions: {{ plan.subscriptions|length }} +
+ {% endif %} + {% endfor %} + + +
+ +
+
+
+ + + +{% endblock body %} diff --git a/subscribie/models.py b/subscribie/models.py index 02c03c57..268dc715 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -60,6 +60,9 @@ def filter_archived(query): if desc["type"] is Person and "archived-subscribers" in request.path: query = query.filter(entity.archived == 1) return query + elif desc["type"] is User and "assign-managers-to-plan" in request.path: + query = query.execution_options(include_archived=True) + return query elif ( desc["type"] is Person and request.path != "/" @@ -108,6 +111,13 @@ class HasReadOnly(object): read_only = Column(Boolean, nullable=False, default=0) +association_table_plan_to_users = database.Table( + "plan_user_associations", + database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), + database.Column("user_id", database.String, ForeignKey("user.id")), +) + + class User(database.Model): __tablename__ = "user" id = database.Column(database.Integer(), primary_key=True) @@ -118,6 +128,7 @@ class User(database.Model): login_token = database.Column(database.String) password_reset_string = database.Column(database.String()) password_expired = database.Column(database.Boolean(), default=0) + plans = relationship("Plan", secondary=association_table_plan_to_users) def set_password(self, password): self.password = generate_password_hash(password) @@ -604,12 +615,6 @@ class Company(database.Model): ), ) -association_table_plan_to_users = database.Table( - "plan_user_associations", - database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), - database.Column("user_id", database.String, ForeignKey("user.id")), -) - class INTERVAL_UNITS(Enum): DAILY = _("daily") @@ -671,10 +676,13 @@ class Plan(database.Model, HasArchived): price_lists = relationship( "PriceList", secondary=association_table_plan_to_price_lists ) + subscriptions = relationship( + "Subscription", primaryjoin="foreign(Subscription.sku_uuid)==Plan.uuid" + ) managers = relationship( "User", secondary=association_table_plan_to_users, - backref=database.backref("plans", lazy="dynamic"), + backref=database.backref("managers", lazy="dynamic"), ) def getPrice(self, currency): From 70123fffae9d0b81e4093a2a4ea9de3821526e40 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sat, 17 Feb 2024 16:56:08 +0000 Subject: [PATCH 03/15] Fix #1305 when plan archived, its new plan has pointer to prior revision on plan.parent_plan_revision_uuid --- ...6_add_parent_plan_revision_uuid_to_plan.py | 27 +++++++++++++++++++ subscribie/blueprints/admin/__init__.py | 1 + subscribie/models.py | 1 + 3 files changed, 29 insertions(+) create mode 100644 migrations/versions/bb76d2149316_add_parent_plan_revision_uuid_to_plan.py diff --git a/migrations/versions/bb76d2149316_add_parent_plan_revision_uuid_to_plan.py b/migrations/versions/bb76d2149316_add_parent_plan_revision_uuid_to_plan.py new file mode 100644 index 00000000..7b6130e5 --- /dev/null +++ b/migrations/versions/bb76d2149316_add_parent_plan_revision_uuid_to_plan.py @@ -0,0 +1,27 @@ +"""add parent_plan_revision_uuid to plan + +Revision ID: bb76d2149316 +Revises: 48074e6225c6 +Create Date: 2024-02-16 23:22:02.230866 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "bb76d2149316" +down_revision = "48074e6225c6" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("plan", schema=None) as batch_op: + batch_op.add_column( + sa.Column("parent_plan_revision_uuid", sa.String(), nullable=True) + ) + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 23f753b8..63914009 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -544,6 +544,7 @@ def edit(): plan_requirements = PlanRequirements() draftPlan.cancel_at = cancel_at draftPlan.uuid = str(uuid.uuid4()) + draftPlan.parent_plan_revision_uuid = plan.uuid draftPlan.requirements = plan_requirements # Preserve primary icon if exists draftPlan.primary_icon = plan.primary_icon diff --git a/subscribie/models.py b/subscribie/models.py index 697424c2..40423655 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -624,6 +624,7 @@ class Plan(database.Model, HasArchived): id = database.Column(database.Integer(), primary_key=True) created_at = database.Column(database.DateTime, default=datetime.utcnow) uuid = database.Column(database.String(), default=uuid_string) + parent_plan_revision_uuid = database.Column(database.String(), default=uuid_string) title = database.Column(database.String()) description = database.Column(database.String()) interval_unit = database.Column(database.String()) # Charge interval From 81bfbb7c9cefd0f0d6e7646a7568046f71460377 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Mon, 12 Feb 2024 23:16:09 +0000 Subject: [PATCH 04/15] wip Fix #1281 As a shop owner I can assign managers to plan(s) --- ...83d_add_association_table_plan_to_users.py | 47 +++++++++++ subscribie/blueprints/admin/__init__.py | 19 +++++ .../admin/assign_managers_to_plan.html | 84 +++++++++++++++++++ .../admin/templates/admin/dashboard.html | 1 + subscribie/models.py | 22 ++++- 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/d80effffd83d_add_association_table_plan_to_users.py create mode 100644 subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html diff --git a/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py b/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py new file mode 100644 index 00000000..988db015 --- /dev/null +++ b/migrations/versions/d80effffd83d_add_association_table_plan_to_users.py @@ -0,0 +1,47 @@ +"""add association_table_plan_to_users + +Revision ID: d80effffd83d +Revises: 48074e6225c6 +Create Date: 2024-02-12 12:24:44.482877 + +Why? + +Some shop owners want/need to assign managers (users) to +plans. For example large clubs or membership organisations which +assign a 'manager' to one or more plans. + +The plan_user_associations table begins to make possible the +assignment of Users to Plans. Recall that Users (see class User +in models.py) is a shop owner (admin) which may login to the +Subscribie application. + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d80effffd83d" +down_revision = "48074e6225c6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "plan_user_associations", + sa.Column("plan_uuid", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["plan_uuid"], + ["plan.uuid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + ) + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 63914009..a8fcdf20 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -789,6 +789,25 @@ def delete_plan_by_uuid(uuid): return render_template("admin/delete_plan_choose.html", plans=plans) +@admin.route("assign-managers-to-plan") +@login_required +def assign_managers_to_plan(): + """ + assign users (managers) to a plan. + + Some shop owners want/need to assign managers (users) to + plans. For example large clubs or membership organisations which + assign a 'manager' to one or more plans. + + The plan_user_associations table begins to make possible the + assignment of Users to Plans. Recall that Users (see class User + in models.py) is a shop owner (admin) which may login to the + Subscribie application. + """ + users = User.query.all() + return render_template("admin/assign_managers_to_plan.html", users=users) + + @admin.route("/list-documents", methods=["get"]) @login_required def list_documents(): diff --git a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html new file mode 100644 index 00000000..16ecf55b --- /dev/null +++ b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html @@ -0,0 +1,84 @@ +{% extends "admin/layout.html" %} +{% block title %} {{ title }} {% endblock %} + +{% block body %} + +

Assign manager(s) to plans

+ +
+ +
+ +
+
+
+

Allow managers to quickly see plans they're responsible for

+
+

Some larger organisations like to assign people to 'manage' Subscribers who are subscribed to specific plans.
+ For example, if you're a sports club or large membership organisation you can assign specific staff to + become 'managers' of specific plans. This helps them see only the Subscribers they are responsible for.

+ +

+ Once you do so, when they login to their account, the visibility of the Subscribers they see + will be highlighted, and by default any Subscribers they are not a manager of given less Precedence, based + on the Plans they are assigned to as a manager.

+
+

+ Please note this is not a security feature- it does not stop managers being able to access + all plans, it simply makes it easier for a manager of specific plans to quickly see the Subscribers + who are subscribed to the plans they manage. +

+ +

Select a user to assign plan(s) to them.

+ +
+ + {% if confirm is sameas false %} +
+

Are you sure?

+ +
+ {% else %} + + + + + + + + + {% for user in users %} + + + + + {% endfor %} +
Title
+ {{ user.email}} + + + Assign Plan(s) + +
+ {% endif %} + +
+
+
+ +{% endblock body %} diff --git a/subscribie/blueprints/admin/templates/admin/dashboard.html b/subscribie/blueprints/admin/templates/admin/dashboard.html index c5f26a3b..225fa299 100644 --- a/subscribie/blueprints/admin/templates/admin/dashboard.html +++ b/subscribie/blueprints/admin/templates/admin/dashboard.html @@ -15,6 +15,7 @@

Manage My Shop

Checklist

+ Assign managers to plan

Make sure everything's in order. If tasks appear below, then you'll need to complete them to get the most out of your shop. diff --git a/subscribie/models.py b/subscribie/models.py index 40423655..f72e3411 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -579,7 +579,16 @@ class Company(database.Model): database.Column("plan_id", database.Integer, ForeignKey("plan.id")), ) - +""" +Some shop owners want/need to assign managers (users) to +plans. For example large clubs or membership organisations which +assign a 'manager' to one or more plans. + +The plan_user_associations table begins to make possible the +assignment of Users to Plans. Recall that Users (see class User +in models.py) is a shop owner (admin) which may login to the +Subscribie application. +""" association_table_plan_to_price_lists = database.Table( "plan_price_list_associations", database.Column( @@ -595,6 +604,12 @@ class Company(database.Model): ), ) +association_table_plan_to_users = database.Table( + "plan_user_associations", + database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), + database.Column("user_id", database.String, ForeignKey("user.id")), +) + class INTERVAL_UNITS(Enum): DAILY = _("daily") @@ -657,6 +672,11 @@ class Plan(database.Model, HasArchived): price_lists = relationship( "PriceList", secondary=association_table_plan_to_price_lists ) + managers = relationship( + "User", + secondary=association_table_plan_to_users, + backref=database.backref("plans", lazy="dynamic"), + ) def getPrice(self, currency): """Returns a tuple of sell_price and interval_amount of the plan for From d1c6b776fa256f4c3189fc43a474290c782e2831 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Fri, 16 Feb 2024 22:59:33 +0000 Subject: [PATCH 05/15] wip #1281 assign user to plans see also #1305 --- ...452d3a80_add_catagory_user_associations.py | 36 +++++++++++ subscribie/blueprints/admin/__init__.py | 27 ++++++++ .../admin/assign_managers_to_plan.html | 62 +++++++------------ .../templates/admin/user_assign_plan.html | 61 ++++++++++++++++++ subscribie/models.py | 22 ++++--- 5 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 migrations/versions/9083452d3a80_add_catagory_user_associations.py create mode 100644 subscribie/blueprints/admin/templates/admin/user_assign_plan.html diff --git a/migrations/versions/9083452d3a80_add_catagory_user_associations.py b/migrations/versions/9083452d3a80_add_catagory_user_associations.py new file mode 100644 index 00000000..07387ceb --- /dev/null +++ b/migrations/versions/9083452d3a80_add_catagory_user_associations.py @@ -0,0 +1,36 @@ +"""add catagory_user_associations + +Revision ID: 9083452d3a80 +Revises: 48074e6225c6 +Create Date: 2024-02-16 21:38:11.302398 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9083452d3a80" +down_revision = "48074e6225c6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "catagory_user_associations", + sa.Column("category_uuid", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["category_uuid"], + ["category.uuid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + ) + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index a8fcdf20..270d9f95 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -808,6 +808,33 @@ def assign_managers_to_plan(): return render_template("admin/assign_managers_to_plan.html", users=users) +@admin.route("/assign-managers-to-plans//assign-plan", methods=["GET", "POST"]) +@login_required +def user_assign_to_plans(user_id): + user = User.query.get(user_id) + plans = Plan.query.execution_options(include_archived=True) + + if request.method == "POST": + # Remove if not selected + for plan in plans: + if plan in user.plans: + user.plans.remove(plan) + + for plan_id in request.form.getlist("assign"): + plan = Plan.query.execution_options(include_archived=True).get(plan_id) + plan.managers.append(user) + + database.session.commit() + flash("User has been assigned the selected plan(s) as a manager of them") + return redirect(url_for("admin.assign_managers_to_plan")) + + return render_template( + "admin/user_assign_plan.html", + user=user, + plans=plans, + ) + + @admin.route("/list-documents", methods=["get"]) @login_required def list_documents(): diff --git a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html index 16ecf55b..5a5c226f 100644 --- a/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html +++ b/subscribie/blueprints/admin/templates/admin/assign_managers_to_plan.html @@ -9,8 +9,7 @@

Assign manager(s) to plans

@@ -38,44 +37,29 @@

Allow managers to quickly see plans they're responsible for

- {% if confirm is sameas false %} - diff --git a/subscribie/blueprints/admin/templates/admin/user_assign_plan.html b/subscribie/blueprints/admin/templates/admin/user_assign_plan.html new file mode 100644 index 00000000..7a94f14b --- /dev/null +++ b/subscribie/blueprints/admin/templates/admin/user_assign_plan.html @@ -0,0 +1,61 @@ +{% extends "admin/layout.html" %} +{% block title %} {{ title }} {% endblock %} + +{% block body %} + +

Choice Group - Assign Plan

+ +
+ +
+
+
+ +

Assign Plan(s) to user: {{ user.email }}

+

Tick which plan(s) you want to assign the user to

+

Doing so will make these plans(s) and any associated subscriptions appear more prominently for them when they login.

+ +
+
Check all
+
Check none
+
+ {% for plan in plans %} + {% if plan.subscriptions | length > 0 %} +
+
+
+ +
+
+ {{ plan.archived }}| Subscriptions: {{ plan.subscriptions|length }} +
+ {% endif %} + {% endfor %} + + +
+ +
+
+
+ + + +{% endblock body %} diff --git a/subscribie/models.py b/subscribie/models.py index f72e3411..64fa0dbd 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -60,6 +60,9 @@ def filter_archived(query): if desc["type"] is Person and "archived-subscribers" in request.path: query = query.filter(entity.archived == 1) return query + elif desc["type"] is User and "assign-managers-to-plan" in request.path: + query = query.execution_options(include_archived=True) + return query elif ( desc["type"] is Person and request.path != "/" @@ -108,6 +111,13 @@ class HasReadOnly(object): read_only = Column(Boolean, nullable=False, default=0) +association_table_plan_to_users = database.Table( + "plan_user_associations", + database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), + database.Column("user_id", database.String, ForeignKey("user.id")), +) + + class User(database.Model): __tablename__ = "user" id = database.Column(database.Integer(), primary_key=True) @@ -118,6 +128,7 @@ class User(database.Model): login_token = database.Column(database.String) password_reset_string = database.Column(database.String()) password_expired = database.Column(database.Boolean(), default=0) + plans = relationship("Plan", secondary=association_table_plan_to_users) def set_password(self, password): self.password = generate_password_hash(password) @@ -604,12 +615,6 @@ class Company(database.Model): ), ) -association_table_plan_to_users = database.Table( - "plan_user_associations", - database.Column("plan_uuid", database.String, ForeignKey("plan.uuid")), - database.Column("user_id", database.String, ForeignKey("user.id")), -) - class INTERVAL_UNITS(Enum): DAILY = _("daily") @@ -672,10 +677,13 @@ class Plan(database.Model, HasArchived): price_lists = relationship( "PriceList", secondary=association_table_plan_to_price_lists ) + subscriptions = relationship( + "Subscription", primaryjoin="foreign(Subscription.sku_uuid)==Plan.uuid" + ) managers = relationship( "User", secondary=association_table_plan_to_users, - backref=database.backref("plans", lazy="dynamic"), + backref=database.backref("managers", lazy="dynamic"), ) def getPrice(self, currency): From 84c6856cf72489ed58bed9d68554db0b8ad7431e Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sun, 18 Feb 2024 20:56:44 +0000 Subject: [PATCH 06/15] wip Fix #1281 Can add/edit plan managers as a shop owner --- ...111e416_merge_9083452d3a80_bb76d2149316.py | 24 +++++++++++++ subscribie/blueprints/admin/__init__.py | 25 +++++++++++-- .../admin/templates/admin/add_plan.html | 22 ++++++++++++ .../admin/templates/admin/edit.html | 35 ++++++++++++++++++- subscribie/forms.py | 3 ++ 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/535f7111e416_merge_9083452d3a80_bb76d2149316.py diff --git a/migrations/versions/535f7111e416_merge_9083452d3a80_bb76d2149316.py b/migrations/versions/535f7111e416_merge_9083452d3a80_bb76d2149316.py new file mode 100644 index 00000000..af2a3786 --- /dev/null +++ b/migrations/versions/535f7111e416_merge_9083452d3a80_bb76d2149316.py @@ -0,0 +1,24 @@ +"""Merge 9083452d3a80 bb76d2149316 + +Revision ID: 535f7111e416 +Revises: 9083452d3a80, bb76d2149316 +Create Date: 2024-02-17 19:03:56.495742 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '535f7111e416' +down_revision = ('9083452d3a80', 'bb76d2149316') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 270d9f95..367388ae 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -546,6 +546,16 @@ def edit(): draftPlan.uuid = str(uuid.uuid4()) draftPlan.parent_plan_revision_uuid = plan.uuid draftPlan.requirements = plan_requirements + + # Preserve / update managers assigned to plan + managers = [] + managersUserIds = request.form.getlist(f'managers-index-{index}') + for userId in managersUserIds: + user = User.query.get(int(userId)) + managers.append(user) + draftPlan.managers.clear() + draftPlan.managers.extend(managers) + # Preserve primary icon if exists draftPlan.primary_icon = plan.primary_icon @@ -646,7 +656,8 @@ def edit(): database.session.commit() # Save flash("Plan(s) updated.") return redirect(url_for("admin.edit")) - return render_template("admin/edit.html", plans=plans, form=form) + users = User.query.all() + return render_template("admin/edit.html", plans=plans, form=form, users=users) @admin.route("/add", methods=["GET", "POST"]) @@ -654,7 +665,16 @@ def edit(): def add_plan(): form = PlansForm() if form.validate_on_submit(): + # Get managers if assigned + users = User.query.all() + managers = [] + for user in users: + if request.form.get(f"user-{user.id}"): + user = User.query.get(int(request.form.get(f"user-{user.id}"))) + managers.append(user) + draftPlan = Plan() + draftPlan.managers.extend(managers) database.session.add(draftPlan) plan_requirements = PlanRequirements() draftPlan.requirements = plan_requirements @@ -759,7 +779,8 @@ def add_plan(): database.session.commit() flash("Plan added.") return redirect(url_for("admin.dashboard")) - return render_template("admin/add_plan.html", form=form) + users = User.query.all() + return render_template("admin/add_plan.html", form=form, users=users) @admin.route("/delete", methods=["GET"]) diff --git a/subscribie/blueprints/admin/templates/admin/add_plan.html b/subscribie/blueprints/admin/templates/admin/add_plan.html index 5255cd3a..236f4c11 100644 --- a/subscribie/blueprints/admin/templates/admin/add_plan.html +++ b/subscribie/blueprints/admin/templates/admin/add_plan.html @@ -236,6 +236,28 @@

Create a new plan

Time (e.g. midnight) + +
+
+ + +
+ + If you have lots of plans, and have multiple people in your business, you can + assign your shop admin(s) to those plans. + +
+
+ {% for user in users %} +
+ + +
+ {% endfor %} +
+ diff --git a/subscribie/blueprints/admin/templates/admin/edit.html b/subscribie/blueprints/admin/templates/admin/edit.html index 7b3050fb..9ff7fc1d 100644 --- a/subscribie/blueprints/admin/templates/admin/edit.html +++ b/subscribie/blueprints/admin/templates/admin/edit.html @@ -107,7 +107,7 @@

Edit Plans

- +
@@ -233,6 +233,39 @@

Edit Plans

Then, you can send a link to your customer(s) to sign-up to the private plan. + + +
+
+ 0 %} checked {% endif %} /> + + +
+ + If you have lots of plans, and have multiple people in your business, you may want to + assign your shop admin(s) to those plans. + +
+ +
+
+ {% set outer_loop = loop %} + {% for user in users %} +
+ + +
+ {% endfor %} +
+
+ + Share URL diff --git a/subscribie/forms.py b/subscribie/forms.py index d96c8ad4..a2481a74 100644 --- a/subscribie/forms.py +++ b/subscribie/forms.py @@ -79,6 +79,9 @@ class PlansForm(StripWhitespaceForm): description = FieldList( StringField("Description", [validators.optional()], default=False) ) + managers = FieldList( + BooleanField("Managers", [validators.optional()]) + ) class ChoiceGroupForm(FlaskForm): From 70a996c52889541927990800e7d4564e432a3c8b Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sun, 18 Feb 2024 21:35:06 +0000 Subject: [PATCH 07/15] #1281 add get_plan_revisions to Plan model --- subscribie/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/subscribie/models.py b/subscribie/models.py index 64fa0dbd..72a49298 100644 --- a/subscribie/models.py +++ b/subscribie/models.py @@ -6,6 +6,7 @@ from sqlalchemy import event from sqlalchemy import Column from sqlalchemy import Boolean +from sqlalchemy import text from typing import Optional from datetime import datetime @@ -744,6 +745,23 @@ def getPrice(self, currency): ) return sell_price, interval_amount + def get_plan_revisions(self): + textual_sql = text(f""" + WITH RECURSIVE RevisionChain AS + (SELECT id, created_at, uuid, title, parent_plan_revision_uuid + FROM plan WHERE uuid = '{self.uuid}' + UNION ALL + SELECT p.id, p.created_at, p.uuid, p.title, p.parent_plan_revision_uuid + FROM plan p + INNER JOIN RevisionChain rc + ON p.uuid = rc.parent_plan_revision_uuid + ) + SELECT * FROM RevisionChain""") + textual_sql = textual_sql.columns(Plan.id, Plan.created_at, Plan.uuid, Plan.title, Plan.parent_plan_revision_uuid) + orm_sql = database.select(Plan).from_statement(textual_sql) + plan_decendants = database.session.execute(orm_sql).scalars().all() + return plan_decendants + def applyRules(self, rules=[], context={}): """Apply pricelist rules to a given plan From 17b6a343751ce094e0b84deda83320c47164288e Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sun, 18 Feb 2024 21:36:18 +0000 Subject: [PATCH 08/15] #1281 update plan revisions managers during edit save --- subscribie/blueprints/admin/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 367388ae..29c0e5c6 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -556,6 +556,11 @@ def edit(): draftPlan.managers.clear() draftPlan.managers.extend(managers) + # Update plan-revisions with managers + for planRevision in plan.get_plan_revisions(): + planRevision.managers.clear() + planRevision.managers.extend(managers) + # Preserve primary icon if exists draftPlan.primary_icon = plan.primary_icon From 71be2b6bfc89130baf9d0804c73c2300f3fdb2d6 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Mon, 19 Feb 2024 20:17:05 +0000 Subject: [PATCH 09/15] #1281 plan_user_associations merge revisions --- ...5aeba5f_merge_535f7111e416_d80effffd83d.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 migrations/versions/ba57f5aeba5f_merge_535f7111e416_d80effffd83d.py diff --git a/migrations/versions/ba57f5aeba5f_merge_535f7111e416_d80effffd83d.py b/migrations/versions/ba57f5aeba5f_merge_535f7111e416_d80effffd83d.py new file mode 100644 index 00000000..a57276f9 --- /dev/null +++ b/migrations/versions/ba57f5aeba5f_merge_535f7111e416_d80effffd83d.py @@ -0,0 +1,24 @@ +"""Merge 535f7111e416 d80effffd83d + +Revision ID: ba57f5aeba5f +Revises: 535f7111e416, d80effffd83d +Create Date: 2024-02-19 20:09:25.499959 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ba57f5aeba5f' +down_revision = ('535f7111e416', 'd80effffd83d') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass From 9a5c71260b6c95c33076ec9d43fa7d8ff0148253 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Mon, 19 Feb 2024 20:31:18 +0000 Subject: [PATCH 10/15] stats stripe._error.APIConnectionError error handling --- subscribie/blueprints/admin/stats.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/subscribie/blueprints/admin/stats.py b/subscribie/blueprints/admin/stats.py index a75be6b3..c874fc7c 100644 --- a/subscribie/blueprints/admin/stats.py +++ b/subscribie/blueprints/admin/stats.py @@ -102,11 +102,14 @@ def get_number_of_transactions_with_donations(): def get_number_of_recent_subscription_cancellations(): stripe.api_key = get_stripe_secret_key() connect_account_id = get_stripe_connect_account_id() - - subscription_cancellations = stripe.Event.list( - stripe_account=connect_account_id, - limit=100, - types=["customer.subscription.deleted"], - ) + subscription_cancellations = [] + try: + subscription_cancellations = stripe.Event.list( + stripe_account=connect_account_id, + limit=100, + types=["customer.subscription.deleted"], + ) + except (stripe._error.APIConnectionError, stripe._error.AuthenticationError) as e: + log.error(f"stripe._error.*: {e}") return len(subscription_cancellations) From e1f80ce815afa0d4a13c6a30caa2a0d7fe8394bb Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Mon, 19 Feb 2024 23:15:07 +0000 Subject: [PATCH 11/15] #1281 as shop owner, I can show only plans I manage on the subscribers view --- subscribie/blueprints/admin/__init__.py | 31 +++++++++++++++++++ .../admin/templates/admin/subscribers.html | 10 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 29c0e5c6..4e2f91bf 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -14,6 +14,7 @@ request, session, Response, + g, ) from markupsafe import Markup, escape import jinja2 @@ -73,6 +74,7 @@ Category, UpcomingInvoice, Document, + association_table_plan_to_users, ) from .subscription import update_stripe_subscription_statuses from .stats import ( @@ -1337,6 +1339,7 @@ def inject_template_globals(): def subscribers(): action = request.args.get("action") show_active = action == "show_active" + show_plans_i_manage = action == "show_plans_i_manage" subscriber_email = request.args.get("subscriber_email", None) subscriber_name = request.args.get("subscriber_name", None) @@ -1359,6 +1362,33 @@ def subscribers(): elif action == "show_one_off_payments": query = query.filter(Person.subscriptions) query = query.where(Subscription.stripe_subscription_id == None) # noqa: E711 + elif action == "show_plans_i_manage": + """ + Only show plans for which the current logged in user is a manager + of. This orm query runs: + + SELECT plan.title, person.given_name, plan_user_associations.user_id, user.email, user.id -- # noqa: E501 + FROM person + JOIN subscription ON + subscription.person_id = person.id + JOIN plan ON + subscription.sku_uuid = plan.uuid + JOIN plan_user_associations ON + plan.uuid = plan_user_associations.plan_uuid + JOIN user ON + user.id = plan_user_associations.user_id + + WHERE plan_user_associations.user_id = 1 + """ + query = ( + query.join(Subscription, Person.id == Subscription.person_id) + .join(Plan, Subscription.sku_uuid == Plan.uuid) + .join( + association_table_plan_to_users, + Plan.uuid == association_table_plan_to_users.c.plan_uuid, + ) + .where(association_table_plan_to_users.c.user_id == g.user.id) + ) people = query.order_by(desc(Person.created_at)) @@ -1366,6 +1396,7 @@ def subscribers(): "admin/subscribers.html", people=people.all(), show_active=show_active, + show_plans_i_manage=show_plans_i_manage, action=action, ) diff --git a/subscribie/blueprints/admin/templates/admin/subscribers.html b/subscribie/blueprints/admin/templates/admin/subscribers.html index 119214cf..f0bb431c 100644 --- a/subscribie/blueprints/admin/templates/admin/subscribers.html +++ b/subscribie/blueprints/admin/templates/admin/subscribers.html @@ -18,7 +18,7 @@

My Subscribers

{% if show_active %} Show all - {% else %} + {% elif show_plans_i_manage is sameas False %} Show Active {%endif %} {% if settings.donations_enabled %} @@ -28,10 +28,18 @@

My Subscribers

Show Donors {% endif %} {% endif %} + Show only plans I manage

Search...

+ {% if g.user.plans | length == 0 %} +
+ You don't currently have any plans assigned to your user. To assign plans to your user login, + Edit an existing plan or Create a new plan and click 'Managers' when editing or adding + a plan to assign manager(s). +
+ {% endif %} {% if request.args.get("subscriber_email") or request.args.get("subscriber_name") %}
You currently have a search filter active. Click here to show all subscribers again. From 7d7a3fa9f7c25c64528a64cbd96338e11c9c1981 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Tue, 20 Feb 2024 08:15:25 +0000 Subject: [PATCH 12/15] Make Save all explicity on edit plan --- subscribie/blueprints/admin/templates/admin/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subscribie/blueprints/admin/templates/admin/edit.html b/subscribie/blueprints/admin/templates/admin/edit.html index 9ff7fc1d..5ca143ea 100644 --- a/subscribie/blueprints/admin/templates/admin/edit.html +++ b/subscribie/blueprints/admin/templates/admin/edit.html @@ -270,7 +270,7 @@

Edit Plans

- + From cbe30c163894974834cc9af092ef9ce80fade0a4 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Wed, 21 Feb 2024 20:10:32 +0000 Subject: [PATCH 13/15] #1281 assign manager(s) to plan from view subscribers page --- subscribie/blueprints/admin/__init__.py | 42 ++++++++++++++++++- .../admin/templates/admin/dashboard.html | 7 +++- .../admin/templates/admin/subscribers.html | 37 ++++++++++++---- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index 4e2f91bf..2ca47ba6 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -551,7 +551,7 @@ def edit(): # Preserve / update managers assigned to plan managers = [] - managersUserIds = request.form.getlist(f'managers-index-{index}') + managersUserIds = request.form.getlist(f"managers-index-{index}") for userId in managersUserIds: user = User.query.get(int(userId)) managers.append(user) @@ -817,6 +817,43 @@ def delete_plan_by_uuid(uuid): return render_template("admin/delete_plan_choose.html", plans=plans) +@admin.route("assign-manager-to-plan", methods=["POST"]) +@login_required +def assign_manager_to_plan(): + """ + assign user (manager) to a plan. + + Some shop owners want/need to assign managers (users) to + plans. For example large clubs or membership organisations which + assign a 'manager' to one or more plans. + + The plan_user_associations table begins to make possible the + assignment of Users to Plans. Recall that Users (see class User + in models.py) is a shop owner (admin) which may login to the + Subscribie application. + """ + managers = [] + chosen_user_ids = request.form.getlist("user_id") + for chosen_user_id in chosen_user_ids: + user = User.query.where(User.id == chosen_user_id).first() + managers.append(user) + plan_uuid = request.form.get("plan_uuid") + plan = ( + database.session.query(Plan) + .execution_options(include_archived=True) + .where(Plan.uuid == plan_uuid) + .first() + ) + + plan.managers.extend(managers) + + flash("Manager(s) have assigned to the plan") + + database.session.commit() + + return redirect(url_for("admin.subscribers")) + + @admin.route("assign-managers-to-plan") @login_required def assign_managers_to_plan(): @@ -1391,10 +1428,11 @@ def subscribers(): ) people = query.order_by(desc(Person.created_at)) - + users = User.query.all() return render_template( "admin/subscribers.html", people=people.all(), + users=users, show_active=show_active, show_plans_i_manage=show_plans_i_manage, action=action, diff --git a/subscribie/blueprints/admin/templates/admin/dashboard.html b/subscribie/blueprints/admin/templates/admin/dashboard.html index 225fa299..dce87a18 100644 --- a/subscribie/blueprints/admin/templates/admin/dashboard.html +++ b/subscribie/blueprints/admin/templates/admin/dashboard.html @@ -35,7 +35,12 @@

Checklist

Stats

-

You have: {{ num_active_subscribers }} subscribers with active subscriptions.

+ {% if g.user.plans | length > 0 %} +

You manage {{ g.user.plans | length }} plan(s). Show my subscribers

+ {% endif %} +
+

You have: {{ num_active_subscribers }} subscribers with active subscriptions. +

You've had: {{ num_recent_subscription_cancellations }} subscription cancellations in the last 30 days.

You've had: {{ num_subscribers }} subscribers since starting your shop.

You've had: {{ num_one_off_purchases }} people buy a one-off item from your shop.

diff --git a/subscribie/blueprints/admin/templates/admin/subscribers.html b/subscribie/blueprints/admin/templates/admin/subscribers.html index f0bb431c..d92bad6c 100644 --- a/subscribie/blueprints/admin/templates/admin/subscribers.html +++ b/subscribie/blueprints/admin/templates/admin/subscribers.html @@ -16,23 +16,26 @@

My Subscribers

- {% if show_active %} - Show all - {% elif show_plans_i_manage is sameas False %} - Show Active - {%endif %} + Show all + Show all Active {% if settings.donations_enabled %} {% if action == "show_donors" %} - Show all + Show all {% else %} Show Donors {% endif %} {% endif %} - Show only plans I manage + Show only plans I manage

Search...

+ {% if show_plans_i_manage %} +
+

Showing only subscribers with plans you are a manager of

+
+ {% endif %} + {% if g.user.plans | length == 0 %}
You don't currently have any plans assigned to your user. To assign plans to your user login, @@ -91,6 +94,24 @@

Search...

{% for subscription in person.subscriptions %}
  • + {% if subscription.plan.managers | length == 0 %} +
    +
    Assign a manager
    + No manager(s) are assigned to this plan. + + {% for user in users %} +
    + + +
    + {% endfor %} + +
    + +
    + +
    + {% endif %}
    • Title: {{ subscription.plan.title }} @@ -283,7 +304,7 @@

      Search...