liquid_feedback_frontend

view model/member.lua @ 1309:32cc544d5a5b

Cumulative patch for upcoming frontend version 4
author bsw/jbe
date Sun Jul 15 14:07:29 2018 +0200 (2018-07-15)
parents 77dc363fa0be
children faaf9ec3e09c
line source
1 Member = mondelefant.new_class()
2 Member.table = 'member'
4 local function secret_token()
5 local parts = {}
6 for i = 1, 5 do
7 parts[#parts+1] = multirand.string(5, "23456789bcdfghjkmnpqrstvwxyz")
8 end
9 return (table.concat(parts, "-"))
10 end
12 Member:add_reference{
13 mode = "1m",
14 to = "MemberHistory",
15 this_key = 'id',
16 that_key = 'member_id',
17 ref = 'history_entries',
18 back_ref = 'member'
19 }
21 Member:add_reference{
22 mode = '1m',
23 to = "MemberImage",
24 this_key = 'id',
25 that_key = 'member_id',
26 ref = 'images',
27 back_ref = 'member'
28 }
30 Member:add_reference{
31 mode = '1m',
32 to = "Contact",
33 this_key = 'id',
34 that_key = 'member_id',
35 ref = 'contacts',
36 back_ref = 'member',
37 default_order = '"other_member_id"'
38 }
40 Member:add_reference{
41 mode = '1m',
42 to = "Contact",
43 this_key = 'id',
44 that_key = 'member_id',
45 ref = 'foreign_contacts',
46 back_ref = 'other_member',
47 default_order = '"member_id"'
48 }
50 Member:add_reference{
51 mode = '1m',
52 to = "Session",
53 this_key = 'id',
54 that_key = 'member_id',
55 ref = 'sessions',
56 back_ref = 'member',
57 default_order = '"ident"'
58 }
60 Member:add_reference{
61 mode = '1m',
62 to = "Draft",
63 this_key = 'id',
64 that_key = 'author_id',
65 ref = 'drafts',
66 back_ref = 'author',
67 default_order = '"id"'
68 }
70 Member:add_reference{
71 mode = '1m',
72 to = "Suggestion",
73 this_key = 'id',
74 that_key = 'author_id',
75 ref = 'suggestions',
76 back_ref = 'author',
77 default_order = '"id"'
78 }
80 Member:add_reference{
81 mode = '1m',
82 to = "Membership",
83 this_key = 'id',
84 that_key = 'member_id',
85 ref = 'memberships',
86 back_ref = 'member',
87 default_order = '"area_id"'
88 }
90 Member:add_reference{
91 mode = '1m',
92 to = "Interest",
93 this_key = 'id',
94 that_key = 'member_id',
95 ref = 'interests',
96 back_ref = 'member',
97 default_order = '"id"'
98 }
100 Member:add_reference{
101 mode = '1m',
102 to = "Initiator",
103 this_key = 'id',
104 that_key = 'member_id',
105 ref = 'initiators',
106 back_ref = 'member'
107 }
109 Member:add_reference{
110 mode = '1m',
111 to = "Supporter",
112 this_key = 'id',
113 that_key = 'member_id',
114 ref = 'supporters',
115 back_ref = 'member'
116 }
118 Member:add_reference{
119 mode = '1m',
120 to = "Opinion",
121 this_key = 'id',
122 that_key = 'member_id',
123 ref = 'opinions',
124 back_ref = 'member',
125 default_order = '"id"'
126 }
128 Member:add_reference{
129 mode = '1m',
130 to = "Delegation",
131 this_key = 'id',
132 that_key = 'truster_id',
133 ref = 'outgoing_delegations',
134 back_ref = 'truster',
135 -- default_order = '"id"'
136 }
138 Member:add_reference{
139 mode = '1m',
140 to = "Delegation",
141 this_key = 'id',
142 that_key = 'trustee_id',
143 ref = 'incoming_delegations',
144 back_ref = 'trustee',
145 -- default_order = '"id"'
146 }
148 Member:add_reference{
149 mode = '1m',
150 to = "DirectVoter",
151 this_key = 'id',
152 that_key = 'member_id',
153 ref = 'direct_voter',
154 back_ref = 'member',
155 default_order = '"issue_id"'
156 }
158 Member:add_reference{
159 mode = '1m',
160 to = "Vote",
161 this_key = 'id',
162 that_key = 'member_id',
163 ref = 'vote',
164 back_ref = 'member',
165 default_order = '"issue_id", "initiative_id"'
166 }
168 Member:add_reference{
169 mode = '11',
170 to = "MemberProfile",
171 this_key = 'id',
172 that_key = 'member_id',
173 ref = 'profile',
174 back_ref = 'member'
175 }
177 Member:add_reference{
178 mode = '11',
179 to = "MemberSettings",
180 this_key = 'id',
181 that_key = 'member_id',
182 ref = 'settings',
183 back_ref = 'member'
184 }
186 Member:add_reference{
187 mode = 'mm',
188 to = "Member",
189 this_key = 'id',
190 that_key = 'id',
191 connected_by_table = 'contact',
192 connected_by_this_key = 'member_id',
193 connected_by_that_key = 'other_member_id',
194 ref = 'saved_members',
195 }
197 Member:add_reference{
198 mode = 'mm',
199 to = "Member",
200 this_key = 'id',
201 that_key = 'id',
202 connected_by_table = 'contact',
203 connected_by_this_key = 'other_member_id',
204 connected_by_that_key = 'member_id',
205 ref = 'saved_by_members',
206 }
208 Member:add_reference{
209 mode = 'mm',
210 to = "Unit",
211 this_key = 'id',
212 that_key = 'id',
213 connected_by_table = 'privilege',
214 connected_by_this_key = 'member_id',
215 connected_by_that_key = 'unit_id',
216 ref = 'units'
217 }
219 Member:add_reference{
220 mode = 'mm',
221 to = "Area",
222 this_key = 'id',
223 that_key = 'id',
224 connected_by_table = 'membership',
225 connected_by_this_key = 'member_id',
226 connected_by_that_key = 'area_id',
227 ref = 'areas'
228 }
230 Member:add_reference{
231 mode = 'mm',
232 to = "Issue",
233 this_key = 'id',
234 that_key = 'id',
235 connected_by_table = 'interest',
236 connected_by_this_key = 'member_id',
237 connected_by_that_key = 'issue_id',
238 ref = 'issues'
239 }
241 Member:add_reference{
242 mode = 'mm',
243 to = "Initiative",
244 this_key = 'id',
245 that_key = 'id',
246 connected_by_table = 'initiator',
247 connected_by_this_key = 'member_id',
248 connected_by_that_key = 'initiative_id',
249 ref = 'initiated_initiatives'
250 }
252 Member:add_reference{
253 mode = 'mm',
254 to = "Initiative",
255 this_key = 'id',
256 that_key = 'id',
257 connected_by_table = 'supporter',
258 connected_by_this_key = 'member_id',
259 connected_by_that_key = 'initiative_id',
260 ref = 'supported_initiatives'
261 }
263 function Member:build_selector(args)
264 local selector = self:new_selector()
265 if args.active ~= nil then
266 selector:add_where{ "member.active = ?", args.active }
267 end
268 if args.locked ~= nil then
269 selector:add_where{ "member.locked = ?", args.locked }
270 end
271 if args.is_contact_of_member_id then
272 selector:join("contact", "__model_member__contact", "member.id = __model_member__contact.other_member_id")
273 selector:add_where{ "__model_member__contact.member_id = ?", args.is_contact_of_member_id }
274 end
275 if args.voting_right_for_unit_id then
276 selector:join("privilege", "__model_member__privilege", { "member.id = __model_member__privilege.member_id AND __model_member__privilege.voting_right AND __model_member__privilege.unit_id = ?", args.voting_right_for_unit_id })
277 end
278 if args.admin_search then
279 local search_string = "%" .. args.admin_search .. "%"
280 selector:add_where{ "member.identification ILIKE ? OR member.name ILIKE ?", search_string, search_string }
281 end
282 if args.order then
283 if args.order == "id" then
284 selector:add_order_by("id")
285 elseif args.order == "identification" then
286 selector:add_order_by("identification")
287 elseif args.order == "name" then
288 selector:add_order_by("name")
289 else
290 error("invalid order")
291 end
292 end
293 return selector
294 end
296 function Member:lockForReference()
297 self.get_db_conn().query("LOCK TABLE " .. self:get_qualified_table() .. " IN ROW SHARE MODE")
298 end
301 function Member:get_all_by_authority(authority)
303 local members = Member:new_selector()
304 :add_where{ "authority = ?", authority }
305 :add_field("authority_uid")
306 :exec()
308 return members
309 end
311 function Member.object:set_password(password)
312 trace.disable()
314 local hash_prefix
315 local salt_length
317 local function rounds()
318 return multirand.integer(
319 config.password_hash_min_rounds,
320 config.password_hash_max_rounds
321 )
322 end
324 if config.password_hash_algorithm == "crypt_md5" then
325 hash_prefix = "$1$"
326 salt_length = 8
328 elseif config.password_hash_algorithm == "crypt_sha256" then
329 hash_prefix = "$5$rounds=" .. rounds() .. "$"
330 salt_length = 16
332 elseif config.password_hash_algorithm == "crypt_sha512" then
333 hash_prefix = "$6$rounds=" .. rounds() .. "$"
334 salt_length = 16
336 else
337 error("Unknown hash algorithm selected in configuration")
339 end
341 hash_prefix = hash_prefix .. multirand.string(
342 salt_length,
343 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"
344 )
346 local hash = extos.crypt(password, hash_prefix)
348 if not hash or hash:sub(1, #hash_prefix) ~= hash_prefix then
349 error("Password hashing algorithm failed")
350 end
352 self.password = hash
353 self.password_reset_secret = nil
354 self.password_reset_secret_expiry = nil
355 end
357 function Member.object:check_password(password)
358 if type(password) == "string" and type(self.password) == "string" then
359 return extos.crypt(password, self.password) == self.password
360 else
361 return false
362 end
363 end
365 function Member.object_get:password_hash_needs_update()
367 if self.password == nil then
368 return nil
369 end
371 local function check_rounds(rounds)
372 if rounds then
373 rounds = tonumber(rounds)
374 if
375 rounds >= config.password_hash_min_rounds and
376 rounds <= config.password_hash_max_rounds
377 then
378 return false
379 end
380 end
381 return true
382 end
384 if config.password_hash_algorithm == "crypt_md5" then
386 return self.password:sub(1,3) ~= "$1$"
388 elseif config.password_hash_algorithm == "crypt_sha256" then
390 return check_rounds(self.password:match("^%$5%$rounds=([1-9][0-9]*)%$"))
392 elseif config.password_hash_algorithm == "crypt_sha512" then
394 return check_rounds(self.password:match("^%$6%$rounds=([1-9][0-9]*)%$"))
396 else
397 error("Unknown hash algorithm selected in configuration")
399 end
401 end
403 function Member.object_get:published_contacts()
404 return Member:new_selector()
405 :join('"contact"', nil, '"contact"."other_member_id" = "member"."id"')
406 :add_where{ '"contact"."member_id" = ?', self.id }
407 :add_where("public")
408 :exec()
409 end
411 function Member:by_login_and_password(login, password)
413 local function prepare_login_selector()
414 local selector = self:new_selector()
415 selector:add_field({ "now() > COALESCE(last_delegation_check, activated) + ?::interval", config.check_delegations_interval_hard }, "needs_delegation_check_hard")
416 selector:add_where('NOT "locked"')
417 selector:optional_object_mode()
418 return selector
419 end
421 local function do_local_login()
422 local selector = prepare_login_selector()
423 selector:add_where{'"login" = ?', login }
424 local member = selector:exec()
425 if member and member:check_password(password) then
426 return member
427 else
428 return nil
429 end
430 end
432 if config.ldap.member then
434 -- Let's check the users credentials against the LDAP
435 local ldap_entry, ldap_err = ldap.check_credentials(login, password)
437 -- Is the user already registered as member?
438 local uid
439 local selector = prepare_login_selector()
441 -- Get login name from LDAP entry
442 if ldap_entry then
443 uid = config.ldap.member.uid_map(ldap_entry)
444 selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", uid }
446 -- or build it from the login
447 else
448 login = config.ldap.member.login_normalizer(login)
449 selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", login }
450 end
452 local member = selector:exec()
453 -- The member is already registered
454 if member then
456 -- The credentials entered by the user are invalid
457 if ldap_err == "invalid_credentials" then
459 -- Check if the user tried a cached password (which is invalid now)
460 if config.ldap.member.cache_passwords and member:check_password(password) then
461 member.password = nil
462 member:save()
463 end
465 -- Try a regular login
466 return do_local_login()
468 end
470 -- The credentials were accepted by the LDAP server and no error occured
471 if ldap_entry and not ldap_err then
473 -- Cache the password (if feature enabled)
474 if config.ldap.member.cache_passwords and not member:check_password(password) then
475 member:set_password(password)
476 end
478 -- update the member attributes and privileges from LDAP
479 local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid)
480 if not err then
481 local err = member:try_save()
482 if err then
483 return nil, "member_save_error", err
484 end
485 local succes, err, err2 = ldap.update_member_privileges(member, ldap_entry)
486 if err then
487 return nil, "update_member_privileges_error", err, err2
488 end
489 return member
490 end
492 end
494 -- Some kind of LDAP error happened, if cached password are enabled,
495 -- check user credentials against the cache
496 if config.ldap.member.cache_passwords and member:check_password(password) then
498 -- return the successfully logged in member
499 return member
501 end
503 -- The member is not registered
504 elseif config.ldap.member.registration and ldap_entry and not ldap_err then
505 -- Automatic registration ("auto")
506 if config.ldap.member.registration == "auto" then
507 member = Member:new()
508 member.authority = "ldap"
509 local ldap_login
510 if config.ldap.member.cache_passwords then
511 if config.ldap.member.login_normalizer then
512 ldap_login = config.ldap.member.login_normalizer(login)
513 else
514 ldap_login = login
515 end
516 end
517 -- TODO change this when SQL layers supports hstore
518 member.authority_uid = uid
519 member.authority_login = ldap_login
520 member.activated = "now"
521 member.last_activity = "now"
522 if config.ldap.member.cache_passwords then
523 member:set_password(password)
524 end
525 local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid)
526 if not err then
527 local err = member:try_save()
528 if err then
529 return nil, "member_save_error", err
530 end
531 local success, err, err2 = ldap.update_member_privileges(member, ldap_entry)
532 if err then
533 return nil, "update_member_privileges_error", err, err2
534 end
535 return member
536 end
538 -- No automatic registration
539 else
540 return nil, "ldap_credentials_valid_but_no_member", uid
541 end
542 end
544 end
546 return do_local_login()
548 end
550 function Member:by_ids(ids)
551 local selector = self:new_selector()
552 selector:add_where{'"id" IN ($)', { ids } }
553 return selector:exec()
554 end
556 function Member:by_login(login)
557 local selector = self:new_selector()
558 selector:add_where{'"login" = ?', login }
559 selector:optional_object_mode()
560 return selector:exec()
561 end
563 function Member:by_name(name)
564 local selector = self:new_selector()
565 selector:add_where{'"name" = ?', name }
566 selector:optional_object_mode()
567 return selector:exec()
568 end
570 function Member:get_search_selector(search_string)
571 return self:new_selector()
572 :add_field( {'"highlight"("member"."name", ?)', search_string }, "name_highlighted")
573 :add_where{ '"member"."text_search_data" @@ "text_search_query"(?)', search_string }
574 :add_where("activated NOTNULL AND active")
575 end
577 function Member.object:send_password_reset_mail()
578 trace.disable()
579 if not self.notify_email then
580 return false
581 end
582 self.password_reset_secret = secret_token()
583 local expiry = db:query("SELECT now() + '1 days'::interval as expiry", "object").expiry
584 self.password_reset_secret_expiry = expiry
585 self:save()
586 local content = slot.use_temporary(function()
587 slot.put(_"Hello " .. self.name .. ",\n\n")
588 slot.put(_"to reset your password please click on the following link:\n\n")
589 slot.put(request.get_absolute_baseurl() .. "index/reset_password.html?secret=" .. self.password_reset_secret .. "\n\n")
590 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
591 slot.put(request.get_absolute_baseurl() .. "index/reset_password.html\n\n")
592 slot.put(_"On that page please enter the reset code:\n\n")
593 slot.put(self.password_reset_secret .. "\n\n")
594 end)
595 local success = net.send_mail{
596 envelope_from = config.mail_envelope_from,
597 from = config.mail_from,
598 reply_to = config.mail_reply_to,
599 to = self.notify_email,
600 subject = config.mail_subject_prefix .. _"Password reset request",
601 content_type = "text/plain; charset=UTF-8",
602 content = content
603 }
604 return success
605 end
607 function Member.object:send_invitation(template_file, subject)
608 trace.disable()
609 self.invite_code = secret_token()
610 self:save()
612 local subject = subject
613 local content
614 local baseurl = request.get_absolute_baseurl() .. "index/register.html"
615 local url = baseurl .. "?skip=1&code=" .. self.invite_code
616 if template_file then
617 local fh = io.open(template_file, "r")
618 content = fh:read("*a")
619 content = content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
620 elseif config.invitation_mail then
621 subject = config.invitation_mail.subject
622 content = config.invitation_mail.content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
623 else
624 subject = config.mail_subject_prefix .. _"Invitation to LiquidFeedback"
625 content = slot.use_temporary(function()
626 slot.put(_"Hello\n\n")
627 slot.put(_"You are invited to LiquidFeedback. To register please click the following link:\n\n")
628 slot.put(url .. "\n\n")
629 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
630 slot.put(baseurl .. "\n\n")
631 slot.put(_"On that page please enter the invite key:\n\n")
632 slot.put(self.invite_code .. "\n\n")
633 end)
634 end
636 local success = net.send_mail{
637 envelope_from = config.mail_envelope_from,
638 from = config.mail_from,
639 reply_to = config.mail_reply_to,
640 to = self.notify_email_unconfirmed or self.notify_email,
641 subject = subject,
642 content_type = "text/plain; charset=UTF-8",
643 content = content
644 }
645 return success
646 end
648 function Member.object:set_notify_email(notify_email)
649 trace.disable()
650 local expiry = db:query("SELECT now() + '7 days'::interval as expiry", "object").expiry
651 self.notify_email_unconfirmed = notify_email
652 self.notify_email_secret = secret_token()
653 self.notify_email_secret_expiry = expiry
654 local content = slot.use_temporary(function()
655 slot.put(_"Hello " .. self.name .. ",\n\n")
656 slot.put(_"Please confirm your email address by clicking the following link:\n\n")
657 slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html?secret=" .. self.notify_email_secret .. "\n\n")
658 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
659 slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html\n\n")
660 slot.put(_"On that page please enter the confirmation code:\n\n")
661 slot.put(self.notify_email_secret .. "\n\n")
662 end)
663 local success = net.send_mail{
664 envelope_from = config.mail_envelope_from,
665 from = config.mail_from,
666 reply_to = config.mail_reply_to,
667 to = self.notify_email_unconfirmed,
668 subject = config.mail_subject_prefix .. _"Email confirmation request",
669 content_type = "text/plain; charset=UTF-8",
670 content = content
671 }
672 if success then
673 local lock_expiry = db:query("SELECT now() + '1 hour'::interval AS lock_expiry", "object").lock_expiry
674 self.notify_email_lock_expiry = lock_expiry
675 end
676 self:save()
677 return success
678 end
680 function Member.object:get_setting(key)
681 return Setting:by_pk(self.id, key)
682 end
684 function Member.object:get_setting_value(key)
685 local setting = Setting:by_pk(self.id, key)
686 if setting then
687 return setting.value
688 end
689 end
691 function Member.object:set_setting(key, value)
692 local setting = self:get_setting(key)
693 if not setting then
694 setting = Setting:new()
695 setting.member_id = self.id
696 setting.key = key
697 end
698 setting.value = value
699 setting:save()
700 end
702 function Member.object:get_setting_maps_by_key(key)
703 return SettingMap:new_selector()
704 :add_where{ "member_id = ?", self.id }
705 :add_where{ "key = ?", key }
706 :add_order_by("subkey")
707 :exec()
708 end
710 function Member.object:get_setting_map_by_key_and_subkey(key, subkey)
711 return SettingMap:new_selector()
712 :add_where{ "member_id = ?", self.id }
713 :add_where{ "key = ?", key }
714 :add_where{ "subkey = ?", subkey }
715 :add_order_by("subkey")
716 :optional_object_mode()
717 :exec()
718 end
720 function Member.object:set_setting_map(key, subkey, value)
721 setting_map = self:get_setting_map_by_key_and_subkey(key, subkey)
722 if not setting_map then
723 setting_map = SettingMap:new()
724 setting_map.member_id = self.id
725 setting_map.key = key
726 setting_map.subkey = subkey
727 end
728 setting_map.value = value
729 setting_map:save()
730 end
732 function Member.object_get:notify_email_locked()
733 return(
734 Member:new_selector()
735 :add_where{ "id = ?", app.session.member.id }
736 :add_where("notify_email_lock_expiry > now()")
737 :count() == 1
738 )
739 end
741 function Member.object_get:units_with_voting_right()
742 return(Unit:new_selector()
743 :join("privilege", nil, { "privilege.unit_id = unit.id AND privilege.member_id = ? AND privilege.voting_right", self.id })
744 :exec()
745 )
746 end
748 function Member.object:ui_field_text(args)
749 args = args or {}
750 if app.session:has_access("authors_pseudonymous") then
751 -- ugly workaround for getting html into a replaced string and to the user
752 ui.container{label = args.label, label_attr={class="ui_field_label"}, content = function()
753 slot.put(string.format('<span><a href="%s">%s</a></span>',
754 encode.url{
755 module = "member",
756 view = "show",
757 id = self.id,
758 },
759 encode.html(self.name)))
760 end
761 }
762 else
763 ui.field.text{ label = args.label, value = _"[not displayed public]" }
764 end
765 end
767 local function populate_units_with_initiative_right_hash(self)
768 if not self.__units_with_initiative_right_hash then
769 local privileges = Privilege:new_selector()
770 :add_where{ "member_id = ?", self.id }
771 :add_where("initiative_right")
772 :exec()
773 self.__units_with_initiative_right_hash = {}
774 for i, privilege in ipairs(privileges) do
775 self.__units_with_initiative_right_hash[privilege.unit_id] = true
776 end
777 end
778 end
780 function Member.object:has_initiative_right_for_unit_id(unit_id)
781 populate_units_with_initiative_right_hash(self)
782 return self.__units_with_initiative_right_hash[unit_id] and true or false
783 end
785 function Member.object_get:has_initiative_right()
786 populate_units_with_initiative_right_hash(self)
787 for k, v in pairs(self.__units_with_initiative_right_hash) do
788 return true
789 end
790 return false
791 end
793 local function populate_units_with_voting_right_hash(self)
794 if not self.__units_with_voting_right_hash then
795 local privileges = Privilege:new_selector()
796 :add_where{ "member_id = ?", self.id }
797 :add_where("voting_right")
798 :exec()
799 self.__units_with_voting_right_hash = {}
800 for i, privilege in ipairs(privileges) do
801 self.__units_with_voting_right_hash[privilege.unit_id] = true
802 end
803 end
804 end
806 function Member.object_get:has_voting_right()
807 populate_units_with_voting_right_hash(self)
808 for k, v in pairs(self.__units_with_voting_right_hash) do
809 return true
810 end
811 return false
812 end
814 function Member.object:has_voting_right_for_unit_id(unit_id)
815 populate_units_with_voting_right_hash(self)
816 return self.__units_with_voting_right_hash[unit_id] and true or false
817 end
819 function Member.object:has_polling_right_for_unit_id(unit_id)
820 if not self.__units_with_polling_right_hash then
821 local privileges = Privilege:new_selector()
822 :add_where{ "member_id = ?", self.id }
823 :add_where("polling_right")
824 :exec()
825 self.__units_with_polling_right_hash = {}
826 for i, privilege in ipairs(privileges) do
827 self.__units_with_polling_right_hash[privilege.unit_id] = true
828 end
829 end
830 return self.__units_with_polling_right_hash[unit_id] and true or false
831 end
833 function Member.object:get_delegatee_member(unit_id, area_id, issue_id)
834 local selector = Member:new_selector()
835 if unit_id then
836 selector:join("delegation", nil, { "delegation.trustee_id = member.id AND delegation.scope = 'unit' AND delegation.unit_id = ? AND delegation.truster_id = ?", unit_id, self.id })
837 end
838 selector:optional_object_mode()
839 return selector:exec()
840 end
842 function Member.object:delete()
843 db:query{ "SELECT delete_member(?)", self.id }
844 end
846 function Member.object_get:display_name()
847 if self.identification then
848 return self.identification
849 elseif self.name then
850 return self.name
851 else
852 return "Member #" .. self.id
853 end
854 end

Impressum / About Us