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') }} +
+ {{ form.hidden_tag() }} +
+ {{ form.has_progress(class_="form-check-input") }} + {{ form.has_progress.label(class_="form-check-label") }} + {% if form.has_progress.errors %} +
    + {% for error in form.has_progress.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+ + + +
+ +
+ {{ form.note(class_="form-control", rows="3") }} + {{ form.note.description }} + {% if form.note.errors %} +
    + {% for error in form.note.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+
+ +
+
+ {{ form.submit(class_="btn btn-primary") }} + + + Swap role + +
+
+
+ {% 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`bww7QYz0nt&#sU!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