From 9a097c23d19b3b36989a46397ce43a57888283f9 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 6 Apr 2026 09:00:33 +0000
Subject: [PATCH] feat: allow user role updates from project posts
- Added a toggleable role multi-select field to the project post form.
- Selecting roles in the post form permanently updates the user's profile.
- Automated "Role swap" activity is recorded when roles are updated.
- Project timeline now displays first-letter badges for user roles next to usernames.
- Improved custom form rendering in `projectpost.html` with error reporting.
- Updated `Activity.data` to include user roles for frontend display.
Co-authored-by: loleg <31819+loleg@users.noreply.github.com>
---
dribdat/public/forms.py | 4 ++
dribdat/public/project.py | 15 +++++
dribdat/templates/public/projectlog.html | 5 ++
dribdat/templates/public/projectpost.html | 72 +++++++++++++++++++++-
dribdat/user/models.py | 7 ++-
flask.log | Bin 0 -> 85581 bytes
6 files changed, 101 insertions(+), 2 deletions(-)
create mode 100644 flask.log
diff --git a/dribdat/public/forms.py b/dribdat/public/forms.py
index 7af94988..e9976a6c 100644
--- a/dribdat/public/forms.py
+++ b/dribdat/public/forms.py
@@ -8,6 +8,7 @@
StringField,
TextAreaField,
SelectField,
+ SelectMultipleField,
HiddenField,
)
from wtforms.fields import TimeField, DateField, URLField, DateTimeLocalField
@@ -163,6 +164,9 @@ class ProjectPost(FlaskForm):
id = HiddenField("id")
has_progress = BooleanField("Level up")
+ roles = SelectMultipleField(
+ "Roles", coerce=int, description="Choose one or more team roles for yourself."
+ )
note = TextAreaField(
"How are the vibes in your team right now?",
[length(max=1024)],
diff --git a/dribdat/public/project.py b/dribdat/public/project.py
index 0a807c4b..a852702b 100644
--- a/dribdat/public/project.py
+++ b/dribdat/public/project.py
@@ -205,6 +205,10 @@ def project_post(project_id):
stage, all_valid = validateProjectData(project)
form = ProjectPost(obj=project, next=request.args.get("next"))
+ # Populate roles
+ from dribdat.user.models import Role
+ form.roles.choices = [(r.id, r.name) for r in Role.query.order_by("name")]
+
# Apply random questions
form.note.label.text = drib_question()
@@ -214,6 +218,9 @@ def project_post(project_id):
# if form.is_submitted() and timelimit(thelastact):
# flash("Please wait a minute before posting", 'warning')
+ if not form.is_submitted():
+ form.roles.data = [r.id for r in current_user.roles]
+
if form.is_submitted() and not form.note.data:
# Empty submission
flash("Please add something to your note", "warning")
@@ -224,9 +231,17 @@ def project_post(project_id):
if stageProjectToNext(project):
flash("Level up! You are at stage '%s'" % project.phase, "info")
+ # Update user roles
+ new_roles = Role.query.filter(Role.id.in_(form.roles.data)).all()
+ if set(new_roles) != set(current_user.roles):
+ current_user.roles = new_roles
+ current_user.save()
+ project_action(project_id, "update", action="post", text="🔄 Role swap")
+
# Update project data
del form.id
del form.has_progress
+ del form.roles
# Process form
form.populate_obj(project)
project.update_now()
diff --git a/dribdat/templates/public/projectlog.html b/dribdat/templates/public/projectlog.html
index 4dca8b55..ae051082 100644
--- a/dribdat/templates/public/projectlog.html
+++ b/dribdat/templates/public/projectlog.html
@@ -138,6 +138,11 @@
{{s.title}}
{{ s.author }}
+ {% if s.user_roles %}
+ {% for r in s.user_roles %}
+ {{r[0]|upper}}
+ {% endfor %}
+ {% endif %}
{% endif %}
{% if s.ref_url %}
diff --git a/dribdat/templates/public/projectpost.html b/dribdat/templates/public/projectpost.html
index e9f7dd05..52087982 100644
--- a/dribdat/templates/public/projectpost.html
+++ b/dribdat/templates/public/projectpost.html
@@ -111,7 +111,72 @@
{% if stage %}
- {{ render_form(url_for('project.project_post', project_id=project.id), form, formid='projectPost') }}
+
+
{% else %}
{{ render_form(url_for('project.project_comment', project_id=project.id), form, formid='projectPost') }}
{% endif %}
@@ -135,6 +200,11 @@
{{ s.author }}
+ {% if s.user_roles %}
+ {% for r in s.user_roles %}
+ {{r[0]|upper}}
+ {% endfor %}
+ {% endif %}
{% endif %}
diff --git a/dribdat/user/models.py b/dribdat/user/models.py
index 09fa1689..0513c3b2 100644
--- a/dribdat/user/models.py
+++ b/dribdat/user/models.py
@@ -1096,6 +1096,7 @@ def all_dribs(self, limit=50, by_name=None, only_posts=False):
"title": title,
"text": text,
"author": author,
+ "user_roles": a.data.get("user_roles"),
"name": a.name,
"date": a.timestamp,
"timesince": a.data["timesince"],
@@ -1455,7 +1456,10 @@ class Activity(PkModel):
@property
def data(self):
"""Get JSON representation."""
- localtime = self.timestamp.replace(tzinfo=current_app.tz) # pyright: ignore
+ try:
+ localtime = self.timestamp.replace(tzinfo=current_app.tz) # pyright: ignore
+ except Exception:
+ localtime = self.timestamp
a = {
"id": self.id,
"time": int(mktime(self.timestamp.timetuple())),
@@ -1469,6 +1473,7 @@ def data(self):
if self.user:
a["user_id"] = self.user.id
a["user_name"] = self.user.username
+ a["user_roles"] = [r.name for r in self.user.roles]
if self.project:
a["project_id"] = self.project.id
a["project_name"] = self.project.name
diff --git a/flask.log b/flask.log
new file mode 100644
index 0000000000000000000000000000000000000000..99d02a0a7deabf3a537dac5fbc0d386bf8249f59
GIT binary patch
literal 85581
zcmeI%TTkOi7Qpd+&ZjuaYD8d3oCIhlXxPz6%j`bww7QYz0ntsU!x+<*BN`WPAI&
zPub3mOViK;^gR3nQWD20pYyAWt5UKf@3p;3va$S}Dt9R=&t*%^ya_K_`FyJ;JMu3*
zn2lv>hWbd%XjJ=YIJDY1QK{8xIF}@onbWnm#(Ftux7(7q#W5MoMPy=*eH(RJoq9N-EzQ~!N^oBJB#vU|qS#EQCToS4KaS$VUOeo!pP}T7RlyhT$G960
z5BFmImF2$u<^6q0@3p%j>9azp+1?NTE2rJ>d);$6dH3#@camhH^883nevftT6O&2!
zkK`;sp4U#TiuFLnm(rLT=f&z+XI^5Jrjjb>w~uNfKPRb{dNeUp9bL>)?V?t!;)#y(
zd2Y0~^XMwEex_2LUAbszlfh8=X7AL$x4*V0&3wMs>U5(t8H6wRiOI^hF7djVhbyab
zC~y=mb{T~@Tlu`cD`}Ex=^S)AyG8idZk+VPXCV{`5xU%DPRp@4osL?gS(^5TiOZFb
zCw;4bn`!48+vT~%`HRPg2Oi%p#a^dZW_WKBRk~NpL?IlEZW6X
zUFvY4s{F>Nv7_wR?aI#1<#q9DdwT<7S?OPRzU%dhNVgh$uXFh5mhez(znfEes1SJXEuyHEu9T>lVtvUyRoxVo={f4$lt}!
zbA2SqI5SqCWu~dJdDE6n>b$(xBL;%c;N3jDrVPz;Iod_}Y>zsPHy(;`eZiL>ffoTK+4x2h)i``B=*@gW&nyyZ1d?4@X0;poa^}N0
zaHDJ9)V%ZV+h@C1HP}T;RhGplyiBXL$n|b=cam8gc%7UslI%itqi_>WtjVU~KE7Fp
z-QGdj53AtItQOsFefr`Sv%b3&7555v{|6)Q?N{}2JGiQ1R1&5FR$tpNo#^}JY^EQk
zckyMznVF6CarLy3%Vx)2)~Z`};i8kZCVrYO=OKHgn>Tqg)mJ)QwS&;QhgGNk_?;xv
z+*mL5sfw*}Q9rllLdU);NR*S}fPD9BG2`ss`etujxPunLhui*+Rdg@W#?k7-1=mrfd6|NCKtun+SzY!fBIC9{`pw@
zmmFpnO?{N4VS=-Hr|2yvyF(M2aTR$PZ!(fFuWq<)`Qf;DHTDzvM|h#H)x4OTmyfDJ
z)lS1L@WfhUkHid$<|k%U-WO%-zb4bg$6GQ9jWIK2q1!bU1)uASf%X+fzWdfaEDBf@
zR4ZT>1uP0~jPUEh>NSTa76sB_QLy^SX=5V&3NxkqmoqF1*6S7)1uP1lby4tyZIq1-
zje{54g4qi@8(*`z^QA4ACzq@8Br2DnQ9QHZhZG-;wU3Ieq?y~0c;c`1`@nb>M
ziSl9-=IPs7tCAkQt@YU4Z`ZooQ8)
z4a{})Kb;N2eu{QsN37V~e1^cEz4AAj>oV-cb2FYfZ>FtjlC>_rCid@_o$GlU*g<<)
kZm|&;`LNkoym&h9x}XKZGvVGT_sd({j~oluB|nMu59uymM*si-
literal 0
HcmV?d00001