def includeme(config): config.add_view( TOSForm, context=IRoot, name="tos_form", permission=PERM_VIEW, renderer="arche:templates/form.pt", ) config.add_view( AgreedTOSView, context=IUser, name="agreed_tos", permission=PERM_VIEW, renderer="arche_tos:templates/agreed_tos.pt", ) config.add_view( RevokeAgreementForm, context=IUser, name="revoke_agreement", permission=PERM_EDIT, renderer="arche:templates/form.pt", ) config.add_view( ManageTOSView, context=IRoot, name="_manage_tos", permission=PERM_MANAGE_USERS, renderer="arche_tos:templates/manage_tos.pt", ) config.add_view( ListRevokedUsers, context=IRoot, name="_list_revoked_tos_users", permission=PERM_MANAGE_USERS, renderer="arche_tos:templates/list_revoked_users.pt", ) config.add_view( TOSSettings, context=IRoot, name="_tos_settings", permission=PERM_MANAGE_USERS, renderer="arche:templates/form.pt", ) config.add_exception_view(terms_not_accepted, context=TermsNotAccepted) config.add_view_action( agreed_tos_menu_item, "user_menu", "agreed_tos", title=_("Terms of service"), priority=40, ) config.add_view_action( generic_submenu_items, "site_menu", "manage_tos", title=_("Manage TOS"), permission=PERM_MANAGE_USERS, priority=50, view_name="_manage_tos", )
def __call__(self, node, value): allowed_userids = set() for userid in self.root.users.keys(): if principal_has_permisson(self.request, userid, PERM_MANAGE_USERS, context=self.root): allowed_userids.add(userid) non_admins = set(value) - allowed_userids if non_admins: msg = _( "users_arent_admins_error", default= "The following users don't have the permission to manage users on this site: '${users}' " "Perhaps they aren't admins?", mapping={"users": "', '".join(non_admins)}, ) raise colander.Invalid(node, msg) for userid in allowed_userids: if not self.root.users[userid].email: msg = _( "user_bad_email_error", default="${userid} doesn't have an email address set.", mapping={"userid": userid}, ) raise colander.Invalid(node, msg)
class TOSAgreeSchema(colander.Schema): widget = maybe_modal_form agree_check = colander.SchemaNode( colander.Bool(), title=_("I've read the full agreement and I agree to it"), validator=colander.Function( _return, _("You must agree to the terms to use this site.")), )
def agree_success(self, appstruct): self.flash_messages.add(_("Thank you!"), type="success") self.tos_manager.agree_to(self.tos_manager.find_tos()) if self.use_ajax: return Response( render("arche:templates/deform/destroy_modal.pt", {}, request=self.request)) return HTTPFound(location=self.request.resource_url(self.context))
def typed_revoke_description(node, kw): request = kw["request"] return _( "Type '${sentence}' to confirm that you want to do this.", mapping={ "sentence": request.localizer.translate(TYPED_REVOKE_SENTENCE) }, )
def save_success(self, appstruct): if appstruct != self.appstruct(): self.settings.clear() self.settings.update(appstruct) self.flash_messages.add(self.default_success, type="success") else: self.flash_messages.add(_("No changes made")) return self._relocate_response()
def available_languages(node, kw): request = kw["request"] langs = list(request.root.site_settings.get("languages", ())) if request.localizer.locale_name not in langs: langs.insert(0, request.localizer.locale_name) values = [(x, x) for x in langs] values.insert(0, ("", _("Any language"))) return deform.widget.SelectWidget(values=values)
def enable_typed_revoke_description(node, kw): request = kw["request"] return _( "Require typing '${sentence}' on revoke.", mapping={ "sentence": request.localizer.translate(TYPED_REVOKE_SENTENCE) }, )
def tos(self): uid = self.request.params.get("tos", None) if uid: # Current user probably hasn't got view permission on that object as default. # Which is correct, it shouldn't be part of the site. tos = self.request.resolve_uid(uid, perm=None) if not ITOS.providedBy(tos): raise HTTPNotFound(_("Agreement not found")) return tos
class TOSRevokeSchema(colander.Schema): typed_revoke = colander.SchemaNode( colander.String(), title=_("Type confirmation"), description=typed_revoke_description, validator=TypedRevokeValidator, ) checked_revoke = colander.SchemaNode( colander.Bool(), title=_( "I understand the consequences and want to revoke my agreement."), validator=colander.Function( _return, _("Read above and tick here if you want to do this.")), ) current_password = colander.SchemaNode( colander.String(), title=_("Password check due to severe consequences of revoking this"), widget=deform.widget.PasswordWidget(size=20), validator=deferred_current_password_validator, ) def after_bind(self, schema, kw): request = kw["request"] view = kw["view"] tos = view.tos remove_fields = set() # If current user doesn't have a password, we can't really check against that if not request.profile.password: remove_fields.add("current_password") # Don't enforce fields on inactive TOS if tos.wf_state == "enabled": if not tos.check_password_on_revoke: remove_fields.add("current_password") if not tos.check_typed_on_revoke: remove_fields.add("typed_revoke") else: remove_fields.update(["current_password", "typed_revoke"]) if "typed_revoke" not in remove_fields: remove_fields.add("checked_revoke") for k in remove_fields: if k in schema: del schema[k]
def includeme(config): """ Include components In your paster ini file, add the following keys to customize: # Number of seconds to wait before kicking out users that haven't agreed arche_tos.grace_seconds = <int> # Number of seconds to wait between each check arche_tos.check_interval = <int> """ settings = config.registry.settings prefix = "arche_tos.%s" for k in ("grace_seconds", "check_interval"): key = prefix % k if key in settings: val = int(settings[key]) setattr(TOSManager, k, val) config.registry.registerAdapter(TOSManager) config.registry.registerAdapter(AgreedTOS) config.registry.registerAdapter(RevokedTOS) config.registry.registerAdapter(TOSSettings) config.add_subscriber(check_terms, [IBaseView, IViewInitializedEvent]) config.add_subscriber(email_data_consent_managers, IImportantAgreementsRevoked) config.add_ref_guard( protect_enabled_tos, requires=(ITOS,), catalog_result=False, allow_move=True, title=_("This would delete enabled Terms of service"), ) config.add_ref_guard( protect_tos_folder, requires=(IArcheFolder,), catalog_result=False, allow_move=True, title=_( "ref_guard_deleting_marked_folder", default="Deleting the folder marked as base for terms of service isn't allowed. " "See terms of service settings.", ), )
class TOSSettingsSchema(colander.Schema): data_consent_managers = colander.SchemaNode( colander.Set(), title=_("Data Consent Manager(s)"), descruption=_( "Users handling consent issues - must have administrator rights"), widget=UserReferenceWidget(), validator=OnlyAdministratorsWithEmailValidator, ) email_consent_managers = colander.SchemaNode( colander.Bool(), title=_("Notify via email?"), description=_( "email_consent_managers_desc", default= "Email consent managers when a user revokes an important agreement.", ), ) tos_folder = colander.SchemaNode( colander.String(), title=_("Folder to place TOS in"), description=_( "tos_folder_schema_description", default= "If you don't have any folders yet, create one in the root of your site." ), widget=ReferenceWidget(multiple=False, query_params={"type_name": "Folder"}))
def revoke_success(self, appstruct): important_revoked = self.tos_manager.revoke_agreement(self.tos) if important_revoked: msg = _( "revoked_consent_enabled_tos", default="You've revoked your consent. " "Note that this website will not be usable without " "agreeing to these terms.", ) else: msg = _( "revoked_consent_inactive_tos", default= "You've revoked your consent to terms that were marked as inactive.", ) self.tos_manager.clear_checked() self.flash_messages.add(msg, type="danger", require_commit=False, auto_destruct=False) return self.relocate_response( self.request.resource_url(self.profile, "agreed_tos"))
def terms_not_accepted(context, request): headers = forget(request) request.session.invalidate() fm = IFlashMessages(request) fm.add( _("You need to accept the terms to use this site."), type="danger", require_commit=False, auto_destruct=False, ) # Context is the exception return HTTPFound(location=request.resource_url(request.root), headers=headers)
class TOSForm(BaseForm, TOSMixin): type_name = "TOS" schema_name = "agree" title = _("New terms require your attention") buttons = (deform.Button("agree", title=_("Agree")), ) @property def use_ajax(self): return self.request.is_xhr def before_fields(self): values = {"tos_items": self.tos_manager.find_tos(), "view": self} return render("arche_tos:templates/tos_listing.pt", values, request=self.request) def agree_success(self, appstruct): self.flash_messages.add(_("Thank you!"), type="success") self.tos_manager.agree_to(self.tos_manager.find_tos()) if self.use_ajax: return Response( render("arche:templates/deform/destroy_modal.pt", {}, request=self.request)) return HTTPFound(location=self.request.resource_url(self.context))
class TOS(Content, ContextACLMixin): type_name = "TOS" type_title = _("Terms of service") add_permission = 'Add TOS' nav_visible = False listing_visible = True search_visible = False title = "" body = "" collapse_text = False revoke_body = "" lang = "" check_password_on_revoke = False check_typed_on_revoke = False @property def is_active(self): return self.wf_state == "enabled"
def email_data_consent_managers(event): request = event.request root = request.root settings = ITOSSettings(root) if settings.get("email_consent_managers", None): tos_manager = ITOSManager(request) for user in tos_manager.get_consent_managers(): values = { "revoked_user": event.user, "revoked_tos": event.revoked_tos, "user": user, "site_title": root.title, "tos_link": request.resource_url(root, "_manage_tos"), } html = render( "arche_tos:templates/email_revoked_consent.pt", values, request ) subject = _( "revoked_consent_subject", default="Revoked consent notice from ${title}", mapping={"title": root.title}, ) request.send_email(subject, [user.email], html)
def buttons(self): kwargs = {} if self.context.wf_state == "enabled": kwargs["css_class"] = "btn-danger" return (deform.Button("revoke", title=_("Revoke"), **kwargs), button_cancel)
def __call__(self, node, value): if self.request.localizer.translate(TYPED_REVOKE_SENTENCE) != value: raise colander.Invalid(node, _("Wrong sentence"))
def _return(value): return value == True # Will be interpreted as failed check by Function method. class TOSAgreeSchema(colander.Schema): widget = maybe_modal_form agree_check = colander.SchemaNode( colander.Bool(), title=_("I've read the full agreement and I agree to it"), validator=colander.Function( _return, _("You must agree to the terms to use this site.")), ) TYPED_REVOKE_SENTENCE = _("I understand the consequences") @colander.deferred class TypedRevokeValidator(object): def __init__(self, node, kw): self.request = kw["request"] def __call__(self, node, value): if self.request.localizer.translate(TYPED_REVOKE_SENTENCE) != value: raise colander.Invalid(node, _("Wrong sentence")) @colander.deferred def typed_revoke_description(node, kw): request = kw["request"]
class TOSSchema(colander.Schema): title = colander.SchemaNode(colander.String(), title=_("Title")) body = colander.SchemaNode( colander.String(), title=_("Text to agree to"), widget=deform.widget.RichTextWidget(), ) collapse_text = colander.SchemaNode( colander.Bool(), title=_("Collapse agreement text?"), description=_("Causes a read full text link to be displayed instead."), ) revoke_body = colander.SchemaNode( colander.String(), title=_("Consequences of revoking the agreement"), description=_( "revoke_body_description", default="Will be displayed when the revocation form " "is shown to inform of the consequences. " "If the conseqences are severe, please do express that here!", ), widget=deform.widget.RichTextWidget(), missing="", ) lang = colander.SchemaNode( colander.String(), title=_("Only for this language"), description=_( "If set, only show this agreement for users using this lang."), widget=available_languages, default="", missing="", ) check_password_on_revoke = colander.SchemaNode( colander.Bool(), title=_("Require password check on revoke"), description=_("This will only be enforced for enabled TOS"), default=False, ) check_typed_on_revoke = colander.SchemaNode( colander.Bool(), title=enable_typed_revoke_description, description=_("This will only be enforced for enabled TOS"), default=False, )
class RevokeAgreementForm(BaseForm, TOSMixin): type_name = "TOS" schema_name = "revoke" title = _("Revoke agreement") @property def buttons(self): kwargs = {} if self.context.wf_state == "enabled": kwargs["css_class"] = "btn-danger" return (deform.Button("revoke", title=_("Revoke"), **kwargs), button_cancel) @reify def tos(self): uid = self.request.params.get("tos", None) if uid: # Current user probably hasn't got view permission on that object as default. # Which is correct, it shouldn't be part of the site. tos = self.request.resolve_uid(uid, perm=None) if not ITOS.providedBy(tos): raise HTTPNotFound(_("Agreement not found")) return tos def before_fields(self): # List the consequences of revoking this agreement values = {"tos": self.tos, "view": self} return render( "arche_tos:templates/revoke_tos_consequence.pt", values, request=self.request, ) def revoke_success(self, appstruct): important_revoked = self.tos_manager.revoke_agreement(self.tos) if important_revoked: msg = _( "revoked_consent_enabled_tos", default="You've revoked your consent. " "Note that this website will not be usable without " "agreeing to these terms.", ) else: msg = _( "revoked_consent_inactive_tos", default= "You've revoked your consent to terms that were marked as inactive.", ) self.tos_manager.clear_checked() self.flash_messages.add(msg, type="danger", require_commit=False, auto_destruct=False) return self.relocate_response( self.request.resource_url(self.profile, "agreed_tos")) def cancel_success(self, *args): return self.relocate_response( self.request.resource_url(self.profile, "agreed_tos")) cancel_failure = cancel_success