diff --git a/advlabdb/advlabdb_independent_funs.py b/advlabdb/advlabdb_independent_funs.py
index 30b05b4..598bdd2 100644
--- a/advlabdb/advlabdb_independent_funs.py
+++ b/advlabdb/advlabdb_independent_funs.py
@@ -1,7 +1,7 @@
# Functions not dependent on advlabdb
-from flask import flash
-from markupsafe import Markup
+from flask import flash, url_for
+from markupsafe import Markup, escape
def flashRandomPassword(email: str, password: str):
@@ -49,3 +49,44 @@ def str_formatter(view, context, model, name):
return attr.str()
return attr
+
+
+def _details_link(url, attr, attr_formatter):
+ return f"{escape(attr_formatter(attr))}"
+
+
+def _default_attr_formatter(attr):
+ return attr
+
+
+def link_formatter(model, name, endpoint, attr_formatter=_default_attr_formatter):
+ attr = deep_getattr(model, name)
+
+ if attr is None:
+ return ""
+
+ url = url_for(f"{endpoint}.details_view")
+
+ if hasattr(attr, "__iter__") and not isinstance(attr, str):
+ # List of attributes
+ links = (_details_link(url, a, attr_formatter) for a in attr)
+ return Markup(", ".join(links))
+
+ # Single attribute
+ return Markup(_details_link(url, attr, attr_formatter))
+
+
+def link_formatter_factory(endpoint, attr_formatter=_default_attr_formatter):
+ def formatter(view, context, model, name):
+ return link_formatter(model, name, endpoint, attr_formatter)
+
+ return formatter
+
+
+def email_formatter(view, context, model, name):
+ attr = deep_getattr(model, name)
+
+ if attr is None:
+ return ""
+
+ return Markup(f"{escape(attr)}")