From 7cb519a89f1d593ea1627e3a99efaede32c5cf41 Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Fri, 18 Nov 2022 23:04:43 +0100 Subject: [PATCH] feat(usermanager): add user creation and list views --- lumi2/static/images/base/navbar-logo.svg | 103 +++++++++++++++++ lumi2/static/images/base/plus-circle.png | Bin 0 -> 3953 bytes lumi2/static/images/base/plus-circle.svg | 4 + lumi2/static/images/base/plus.png | Bin 0 -> 926 bytes lumi2/static/images/base/plus.svg | 3 + lumi2/templates/base.html | 29 ++++- .../{user_update.html => user_edit.html} | 38 ++++++- lumi2/templates/usermanager/user_list.html | 24 +++- lumi2/templates/usermanager/user_test.html | 16 --- lumi2/templates/usermanager/user_view.html | 43 +++----- lumi2/usermanager.py | 104 +++++++++++++++--- 11 files changed, 293 insertions(+), 71 deletions(-) create mode 100644 lumi2/static/images/base/navbar-logo.svg create mode 100644 lumi2/static/images/base/plus-circle.png create mode 100644 lumi2/static/images/base/plus-circle.svg create mode 100644 lumi2/static/images/base/plus.png create mode 100644 lumi2/static/images/base/plus.svg rename lumi2/templates/usermanager/{user_update.html => user_edit.html} (68%) delete mode 100644 lumi2/templates/usermanager/user_test.html diff --git a/lumi2/static/images/base/navbar-logo.svg b/lumi2/static/images/base/navbar-logo.svg new file mode 100644 index 0000000..08fcadc --- /dev/null +++ b/lumi2/static/images/base/navbar-logo.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + LUMI 2 + + diff --git a/lumi2/static/images/base/plus-circle.png b/lumi2/static/images/base/plus-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..9d72f575178295c7eaf26509087d5dc263716bd4 GIT binary patch literal 3953 zcmV-%503DOP)Hq)$8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H14*p3* zK~#90?VWkB6vY|9e{aDfh*3j4a;TV;Mh-RNl>*O5@W%I*;DPZdO~orFh9IaA1Wzah z5*54>tBgk!Ej|#UXmKeJ6}%F7UQt0YDwYZc5HEcBqkA8)>`wRW96S5o{HnJ0vD4G@ zvHfk&^w;0f(Ad~WYeH2UfZc$u_PsN(3$Q)V$-cG(HV3w{r>z0L1U?5owyzI?Wx!J4 zE#MVkiHJ0(^FK+4hI&^3Rs8|b%f1c(x&vD$bh%Z)B48mfADAy9?r9n6ksCogou1q zV|kopS^-pbci>#$Y+%RQN)wvpz=OaYBGOcAVJeYH1yI$4fg!+gKtqkiiG%sTXkc2_ z9wIZN0QTv<5f~2qw8r8j#S6eyz#~~vghH(a08rIqFi+N#XuX3E&;ytZEKt>>YAp=Z zcm`0_uE1Cz?k{5V3Z)&0ds@eg#8u%^HK87y;2RsSP2Ic@wBJyPn{kC9NBHb}d&?A6DiDxzP z6)+AMAtGyI=r0thD}bu@25tv_6tSy+0}lYRfagSHT~yh^psMYG{eUBZQ-QrAcKHTy zo`}qfDq9#*Qvhqg839~M_0Jxk01pHAh{(K{M2iJg?FO6yoCWMuWjD>hoxtx!VB z3nCQ-P}RMG`+;s%b}<7pO+FzaU&kg`0@#_M4=@ljJ5<9W;IuUC2Z<|ys-A|?>aARD zOK1lE0bDC0&%`cR3aIM7z~w+c!rL>}0vCwL-LVU{0f{Mqsrz2my6`-p7W44Zcjbm>D2aCuD zQLi0&2C(;~>G%)+WMH3s@Yf?E&jI@bk9eN)V_=%9Ms|IRqyYA5|6%;6{qev_BC;~d zbz37MvI4UmKGyS;{eUT|x#{9lztgiY&aFW-026#~ z1->)D)fnqj5MKiQMdTCDK6651jYX&7-aF`uryqt>0aUdMepC7o5n14K(VXhB-cdt6 znvPJ_U0scQT(ZXjuIXedeh(>`z%G}@VSM)hW&)Sh(my{WH)7n=j}2(k!L|!D)&-h^ zZN$!_4q;XUUFu~^*AD(mz;{Xxt-x$jElXdj@Tlr8z{|LOEqjZ|%t{9<-U4i_-bfEw zM%2sTTgI{J;Dd<_8k4dtRU;zr0M~dly(;Kn&@(_ICgVmBO~6eaLS%_P0mm{CuNQ)` znA9~v^t8T+WeHM%;f{`rrryjaK`0?;4?U}iC(2^O-!8(8MG(U(9Vn{+s(K{uDBX$H z$uXyfh{$Y=*KiPhRJH7EUTFpx?!!$Kz@}w!-3zC-mY|K+ zfQY<=d$o3=s%~P;D>noD3b)C3w5M!2>ET)tGyvz6ez=hWSo`srj*e!C$Uno$mXkEr zLO9#e?3~g)g3=k_DBPjV<3h@olO8UI(w!)YST0q7Qyd*E2c8HiR8D$K$Ba=B8;x;` z0$6XKzK)ac%X)l%TgHyf1CC~m)}0^Ncm_Bew;|+j)rppqBKJ6sZ;Q!q1Z=DT$2d-0 zEF%A^R;rwI5RvCG8AgIQy7)(-+=mtTd$mI4q>jrIyXD7TfR>c&uB_XwHg3NaQgQp& z(p6Qz2kU2mgB>S)iiu6nsSg%lawi7S6V@w0PeoH!)PNtHQ{0cd&)=2Y{=@EgsF(C9*FU5 z?Fn>D`g#solCI{p8dFAYsNK0s5?4OrP6gcQUcw|b4vj4w^&}=@r>29S5*;zFcXL&B z&xEh#8oLe!Ocm`YokZn2Of~Pe#wj|;#@ zjz)ErG$CgrTw~{@0G;EJC8q{-b{zkxfXVI|gv(t#C%d@H_$)2p-ZAp61(cILycA%G zqfyfK>YU_oDZr8fZimm#Q4160q>qjEb~*br6=#4Tn{hi#^5LBNq8n!HDzOrHw;&?T zxT8srh)t}Vv~Z2tVi9RBZU!xk*jY~Y__^cwv#>ri%(W?Tn5z0@7SG85cC1_hzw^Og z0k+0es?4bm4#I5)ej2O-h{$`mU5I-}mn$bFT+@G3vGDYU4k6PWC!H2aq?|N4*>U`_ z;va=_A6B57s=5+L=hPTmiry9N-ST7KPyy!PjsZBMIc%T)2;cjyjA*PrN|&x{k#V9Ss+VXiMz_njpaTp zs{kT03wXiNVZShC%5##zR(Pvi{?+iTh&)y9Ku~O8V8uy7mMPB=LBS_=6_{f4{@?7; zwBlG+Q~(i~g2_1>gm;RnRKjP+zi5<=5HYZ&vdTnVAqo+z;IiTxn3BGiOVY` zjaa?du%+RB0=L?fvzf=HW$6J-!qnGUZvXC)Ldup{u;2A< zxU;)Vw9j)D@v-aw4&w_Dlt|(@j1{Ubs&Ct%%#&&@M+64rUiPiR!)ohH5Mk{CfASFO zdTWi!Nf1@-g{g_u-zooA6s5_$h#h ztOd@)Z6e+oxJy;@)H5-os_lS#ai{kE2DqTq=3fc#fwB)1H+eMe3yk;eH^-ZCz_A|f zMv2H=cQZrSU+NIRr*z!;s_L_Z%<(`~M*Pk`H=CB5|NAMr~=YSKV7W9g2uo00}7`wD@ZJ`r@m#j%M2T;`mFx$31 z>wJJWf#XFavj0M)Tfl~>YG+Kg&)q#w`3jRJc1+NAWoyRn>RpOS8}A!%^B>?BBH}6u z=S6HLTM=1`iAndp9c%{N08CR=S1y}YhuzPA6c~;F;6DfSifiy8#u=c9s&)h(CS2;* z&oIhxeQPgLX&;;~#O&yWNy9q}5D{_J0SZOzn>O~o)EBcn@udUCvvrZGw%X7$>qPq^ zW_vx%;7~qIakz^HS+MI<+b+b6`<)qD*ycZ?}`v~Ar;*)&@a8oUA zZ#7YXBC7gBjH^?>n7dvFyp5@R{wl^yy%bYRcMaxCU&;DVu#UjCnB00D?fY(+Y}>`x zuEf7=o(K%|@!m~}nkqmM8{l{)a7e9XNr`7LwS#NEy2*qB6j9YfG5Z6l6p*R`3xJVH zFYUsRDFrB^s`~(c00!Xa6p>|`fti?l{M1?tQ%WWkpop~}48YjV>utm6eN4WkJ4NKp zS_@N&Oe;VUyW}b^w;C}IY*}OlFd4Hvotbr`71W6WY=}*Ya2Uqgb~LrGX3~U7y*eFu zYQrRq^+4SzKq;!a6Q&G%FN|TPJFrbcm-`fBuvrMq1Llj!@`SFH3U#jl<*?pKT`+6; z&Y0(i9e_^u)dAy#QOv2o3iul1m+-NDEeDoiYAP)OUWwBYBP;wLil9Gy&1lkZ00000 LNkvXXu0mjf=a5pO literal 0 HcmV?d00001 diff --git a/lumi2/static/images/base/plus-circle.svg b/lumi2/static/images/base/plus-circle.svg new file mode 100644 index 0000000..66308ef --- /dev/null +++ b/lumi2/static/images/base/plus-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lumi2/static/images/base/plus.png b/lumi2/static/images/base/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..961da2ff52ea1f11d687f7f561e12547143f0c1c GIT binary patch literal 926 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9E(G9b*j{7}U!prB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=GWLo-U3d6?5L+wapU_lxTfe zeoRQ@fLOyUfe2>%%%Gp^4swWGbj@LI6qp#KlD#8UMfjV8V9??X%uKu|c4^vd40sc9 z!}v$v^Eu0XYL=e=He3F@Nc_e3-vfn`@36!%oNxGjb^3nB;|Cl$9;%#AsT9#-JR*_L z^u8h7q4Mluw*z(?q@8~3U3If)*SppiQ(m35XDXZVe@^v*><1dhDw0JvKUEWC_#juY z>A>UpO!@yW>#V)>lVR2VpZe1+b_pln$*7vQ?PU!^&;N(f&nmw)I3C`-rO%1seD<8X zSvg(Pf;b-J^=|$yHG9!g5vConkKfEawk$)ll_7_ZUEv&KM=`@AnFfmiV9t(`HcwuE zZ|&9oRNh}l=j`p6%S~f($W&Yy;rlg?K|hK@O0jxO`fVBY`=?4 zI~&PabIP_g)mSIUi(j&y=grf3$HKB$Y_wKqAO5{_`NpZYxfSQc)&0BAYWIB3v9MPX z70g#z7rZ)l!0f}skL%f9FBAO0c!N*zPvgz)&E^e^8;<@c+pu16&Gz{{`Rx3ShXoG3 zynlZV*MjV%o~`SOTLik6rDZDLXBK$QaKw_Kkk4Vxz%aQO=Ulh=y?CQ$l$TTD1%%vv9Jy|ga(PN?xWr9*S}R>hVxmM-n{crjZ!ADFioJYD@<);T3K0RSPahHn4> literal 0 HcmV?d00001 diff --git a/lumi2/static/images/base/plus.svg b/lumi2/static/images/base/plus.svg new file mode 100644 index 0000000..5b088c0 --- /dev/null +++ b/lumi2/static/images/base/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/lumi2/templates/base.html b/lumi2/templates/base.html index 4379ccb..7b6d7f1 100644 --- a/lumi2/templates/base.html +++ b/lumi2/templates/base.html @@ -23,14 +23,39 @@ + + {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} -
{{ message }}
+
{{ message }}
{% endfor %} {% endif %} {% endwith %} -
+
{% block content %} {% endblock content %}
diff --git a/lumi2/templates/usermanager/user_update.html b/lumi2/templates/usermanager/user_edit.html similarity index 68% rename from lumi2/templates/usermanager/user_update.html rename to lumi2/templates/usermanager/user_edit.html index 29b023e..6a4f250 100644 --- a/lumi2/templates/usermanager/user_update.html +++ b/lumi2/templates/usermanager/user_edit.html @@ -3,11 +3,22 @@ {% block content %}
-

Edit user: {{ username }}

+

{{ heading }}

{{ form.csrf_token }} + {% if not is_update %} +
+ {{ form.username.label(class="form-label") }} + {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} + {% if form.username.errors %} + {% for error in form.username.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endif %}
{{ form.email.label(class="form-label") }} {{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }} @@ -43,6 +54,11 @@
{{ error }}
{% endfor %} {% endif %} + {% if not is_update %} +
+ Leave empty to use the first name. Will be displayed instead of the first name in some applications. +
+ {% endif %}
{{ form.password.label(class="form-label") }} @@ -52,6 +68,13 @@
{{ error }}
{% endfor %} {% endif %} +
+ {% if is_update %} + Must be at least 8 characters long. Leave empty to keep the current password. + {% else %} + Must be at least 8 characters long. + {% endif %} +
{{ form.password_confirmation.label(class="form-label") }} @@ -70,11 +93,24 @@
{{ error }}
{% endfor %} {% endif %} +
+ {% if is_update %} + Only JPEG files can be used. Leave empty to keep the current picture. + {% else %} + Optional but recommended. Only JPEG files can be used. + {% endif %} +
+ {% if is_update %} Cancel + {% else %} + Cancel + {% endif %} {{ form.submit(class_="btn btn-primary") }}
diff --git a/lumi2/templates/usermanager/user_list.html b/lumi2/templates/usermanager/user_list.html index e031691..91d1e61 100644 --- a/lumi2/templates/usermanager/user_list.html +++ b/lumi2/templates/usermanager/user_list.html @@ -1,34 +1,46 @@ {% extends 'base.html' %} {% block content %} +
+

All users

+
+
- - + + - {% for user in users %} + {% for user in users | sort %} - - + + {% endfor %} diff --git a/lumi2/templates/usermanager/user_test.html b/lumi2/templates/usermanager/user_test.html deleted file mode 100644 index 2392874..0000000 --- a/lumi2/templates/usermanager/user_test.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -
-
-
-
- Column -
-
- Column -
-
-
-
-{% endblock content %} diff --git a/lumi2/templates/usermanager/user_view.html b/lumi2/templates/usermanager/user_view.html index f8e4491..7387093 100644 --- a/lumi2/templates/usermanager/user_view.html +++ b/lumi2/templates/usermanager/user_view.html @@ -1,45 +1,28 @@ {% extends 'base.html' %} {% block content %} -
-
+
+
profile picture for user {{ user.username }}
-
-
-
-

{{ user.username }}

+
+
Username: {{ user.username }}
+
Email: {{ user.email }}
+
First Name: {{ user.first_name }}
+
Last Name: {{ user.last_name }}
+
Nickname: {{ user.display_name }}
-
-
-
Username:
-
{{ user.username }}
-
-
-
Email:
-
{{ user.email }}
-
-
-
First Name:
-
{{ user.first_name }}
-
-
-
Last Name:
-
{{ user.last_name }}
-
-
-
Nickname:
-
{{ user.display_name }}
-
-
-
+
{% endblock content %} diff --git a/lumi2/usermanager.py b/lumi2/usermanager.py index ff128bd..cfc39e9 100644 --- a/lumi2/usermanager.py +++ b/lumi2/usermanager.py @@ -4,7 +4,7 @@ from pathlib import Path from tempfile import TemporaryFile from flask import ( - Blueprint, render_template, abort, request, flash, redirect, url_for + Blueprint, render_template, abort, request, flash, redirect, url_for, current_app ) from PIL import Image, UnidentifiedImageError from flask_wtf import FlaskForm @@ -68,20 +68,14 @@ def user_list(): users=users, ) - -@bp.route("/users/test") -def user_test(): - return render_template( - 'usermanager/user_test.html', - ) - class UserUpdateForm(FlaskForm): @staticmethod def validate_name(form, field) -> None: - try: - User.assert_is_valid_name(field.data) - except InvalidStringFormatException as e: - raise ValidationError(str(e)) + if field.data: + try: + User.assert_is_valid_name(field.data) + except InvalidStringFormatException as e: + raise ValidationError(str(e)) @staticmethod def validate_password(form, field) -> None: @@ -115,11 +109,11 @@ class UserUpdateForm(FlaskForm): [InputRequired(), validate_name] ) display_name = StringField( - 'Nick Name', + 'Nickname', [InputRequired(), validate_name] ) password = PasswordField( - 'Password (leave empty to keep the same)', + 'Password', [ EqualTo('password_confirmation', message='Passwords must match'), validate_password, @@ -137,6 +131,82 @@ class UserUpdateForm(FlaskForm): ) +class UserCreationForm(UserUpdateForm): + @staticmethod + def validate_username(form, field) -> None: + try: + User.assert_is_valid_username(field.data) + except InvalidStringFormatException as e: + raise ValidationError(str(e)) + new_user_dn = f"uid={field.data}," + current_app.config['LDAP_USERS_OU'] + conn = ldap.get_connection() + if ldap.user_exists(conn, new_user_dn): + raise ValidationError("Username is taken.") + conn.unbind() + + username = StringField( + 'Username', + [ + InputRequired('Please enter a username.'), + validate_username + ] + ) + display_name = StringField( + 'Nickname', + [UserUpdateForm.validate_name] + ) + password = PasswordField( + 'Password', + [ + EqualTo('password_confirmation', message='Passwords must match'), + InputRequired('Please enter a password.'), + UserUpdateForm.validate_password, + ], + ) + submit = SubmitField( + 'Create', + ) + + + +@bp.route("/users/create", methods=("GET", "POST")) +def user_create(): + """Creation view for a new User. + + Provides a form which can be used to enter the new user's details. + """ + try: + conn = ldap.get_connection() + except Exception: + abort(500) + + form = UserCreationForm() + if form.validate_on_submit(): + user = User( + form.username.data, + User.generate_password_hash(form.password.data), + form.email.data, + form.first_name.data, + form.last_name.data, + form.display_name.data if form.display_name.data else None, + Image.open(form.picture.data, formats=['JPEG']) if form.picture.data and form.picture.data.filename else None, + ) + + ldap.create_user(conn, user) + user._generate_static_images(force=True) + conn.unbind() + flash(f"User '{user.username}' was created.") + return redirect(url_for('usermanager.user_view', username=user.username)) + + conn.unbind() + return render_template( + 'usermanager/user_edit.html', + form=form, + heading=f"Create a new user", + is_update=False, + ) + + @bp.route("/users/update/", methods=("GET", "POST")) def user_update(username: str): """Update view for a specific User. @@ -183,7 +253,9 @@ def user_update(username: str): conn.unbind() return render_template( - 'usermanager/user_update.html', + 'usermanager/user_edit.html', form=form, - username=user.username + username=user.username, + heading=f"Edit user: {user.username}", + is_update=True, )
Picture UsernameEmail addressFirst Name Last NameFirst NameEmail address Nickname
profile picture for user {{ user.username }} {{ user.username }} {{ user.email }}{{ user.first_name }} {{ user.last_name }}{{ user.first_name }}{{ user.email }} {{ user.display_name }}