# HG changeset patch # User Paul Boddie # Date 1445795630 -3600 # Node ID f04e9ba8fe963f0a3f7f0ef18e912f38f92905f6 # Parent a348efd651846b5ee99a0dea6615bfe81cd55489 Added profile loading and saving, linking to the user profile from the calendar. Changed the FormUtilities.menu method to be able to use supplied chosen values. diff -r a348efd65184 -r f04e9ba8fe96 htdocs/styles.css --- a/htdocs/styles.css Sun Oct 25 01:25:29 2015 +0200 +++ b/htdocs/styles.css Sun Oct 25 18:53:50 2015 +0100 @@ -1,15 +1,29 @@ -body, -#participants, -#pending-requests { +body { background-color: #fff; } +#user-navigation { + float: right; + clear: right; +} + +#user-navigation a { + background-color: #7bf; + color: #000; + text-decoration: none; + font-weight: bold; + padding: 0.25em; + border: 1px dotted #000; +} + #participants { float: right; + clear: right; } #pending-requests { float: left; + clear: left; } #calendar-controls, diff -r a348efd65184 -r f04e9ba8fe96 imiptools/profile.py --- a/imiptools/profile.py Sun Oct 25 01:25:29 2015 +0200 +++ b/imiptools/profile.py Sun Oct 25 18:53:50 2015 +0100 @@ -24,6 +24,10 @@ from imiptools.filesys import fix_permissions, FileBase from os.path import exists, isdir from os import listdir, makedirs +import pytz + +def identity_dict(l): + return dict([(i, i) for i in l]) class Preferences(FileBase): @@ -48,6 +52,51 @@ "permitted_times" : None, } + known_key_choices = { + "TZID" : identity_dict(pytz.all_timezones), + "add_method_response" : { + "add" : "Add events", + "ignore" : "Ignore requests", + "refresh" : "Ask for refreshed event details" + }, + "event_refreshing" : { + "never" : "Do not respond", + "always" : "Always respond" + }, + "freebusy_bundling" : { + "never" : "Never", + "always" : "Always" + }, + "freebusy_messages" : { + "none" : "Do not notify", + "notify" : "Notify" + }, + "freebusy_publishing" : { + "publish" : "Publish", + "no" : "Do not publish" + }, + "freebusy_sharing" : { + "share" : "Share", + "no" : "Do not share" + }, + "incoming" : { + "message-only" : "Original message only", + "message-then-summary" : "Original message followed by a separate summary message", + "summary-then-message" : "Summary message followed by the original message", + "summary-only" : "Summary message only", + "summary-wraps-message" : "Summary message wrapping the original message" + }, + "organiser_replacement" : { + "any" : "Anyone", + "attendee" : "Existing attendees only", + "never" : "Never allow organiser replacement" + }, + "participating" : { + "participate" : "Participate", + "no" : "Do not participate" + } + } + def __init__(self, user, store_dir=None): FileBase.__init__(self, store_dir or config.PREFERENCES_DIR) self.user = user @@ -109,6 +158,8 @@ 'all_known' is set to a true value, with absent entries providing a default of None or any indicated 'default' or, if 'config_default' is set to a true value, the default value from the config module. + + Each entry will have the form (key, value). """ l = [] @@ -116,6 +167,22 @@ l.append((key, self.get(key, default, config_default))) return l + def choices(self, all_known=False, default=None, config_default=False): + + """ + Return all entries in the preferences or all known entries if + 'all_known' is set to a true value, with absent entries providing a + default of None or any indicated 'default' or, if 'config_default' is + set to a true value, the default value from the config module. + + Each entry will have the form (key, value, choices). + """ + + l = [] + for key, value in self.items(all_known, default, config_default): + l.append((key, value, self.known_key_choices.get(key))) + return l + def __getitem__(self, name): "Return the value for 'name', raising a KeyError if absent." diff -r a348efd65184 -r f04e9ba8fe96 imipweb/calendar.py --- a/imipweb/calendar.py Sun Oct 25 01:25:29 2015 +0200 +++ b/imipweb/calendar.py Sun Oct 25 18:53:50 2015 +0100 @@ -20,7 +20,7 @@ """ from datetime import datetime, timedelta -from imiptools.data import get_address, get_uri, uri_parts +from imiptools.data import get_address, get_uri, get_verbose_address, uri_parts from imiptools.dates import format_datetime, get_date, get_datetime, \ get_datetime_item, get_end_of_day, get_start_of_day, \ get_start_of_next_day, get_timestamp, ends_on_same_day, \ @@ -197,6 +197,17 @@ # Page fragment methods. + def show_user_navigation(self): + + "Show user-specific navigation." + + page = self.page + user_attr = self.get_user_attributes() + + page.p(id_="user-navigation") + page.a(get_verbose_address(self.user, user_attr), href=self.link_to("profile"), class_="username") + page.p.close() + def show_requests_on_page(self): "Show requests for the current user." @@ -536,6 +547,7 @@ page.form(method="POST") + self.show_user_navigation() self.show_requests_on_page() self.show_participants_on_page(participants) diff -r a348efd65184 -r f04e9ba8fe96 imipweb/event.py --- a/imipweb/event.py Sun Oct 25 01:25:29 2015 +0200 +++ b/imipweb/event.py Sun Oct 25 18:53:50 2015 +0100 @@ -384,7 +384,7 @@ # Show participation status, editable for the current user. if attendee_uri == self.user: - self.menu("partstat", partstat, self.partstat_items, "partstat") + self.menu("partstat", partstat, self.partstat_items, class_="partstat") # Allow the participation indicator to act as a submit # button in order to refresh the page and show a control for diff -r a348efd65184 -r f04e9ba8fe96 imipweb/profile.py --- a/imipweb/profile.py Sun Oct 25 01:25:29 2015 +0200 +++ b/imipweb/profile.py Sun Oct 25 18:53:50 2015 +0100 @@ -19,31 +19,28 @@ this program. If not, see . """ -from imiptools import config -from imipweb.resource import ResourceClient +from imipweb.resource import FormUtilities, ResourceClient -class ProfilePage(ResourceClient): +class ProfilePage(ResourceClient, FormUtilities): "A request handler for the user profile page." - # See: imiptools.profile + # See: imiptools.config, imiptools.profile - pref_labels = { - "CN" : "Common name", - "LANG" : "Language", - "TZID" : "Time zone/regime", - "add_method_response" : "Respond to messages adding events with...", - "event_refreshing" : "Handle event refresh requests automatically", - "freebusy_bundling" : "Bundle free/busy details with messages", - "freebusy_messages" : "Notify about received free/busy messages", - "freebusy_offers" : "Reserve time periods when making counter-proposals", - "freebusy_publishing" : "Publish free/busy details via the Web", - "freebusy_sharing" : "Share free/busy information at all", - "incoming" : "Incoming calendar messages presented using...", - "organiser_replacement" : "Recognise which kinds of participants as replacement organisers...", - "participating" : "Participate in the calendar system at all?", - "permitted_times" : None, - } + pref_labels = [ + ("participating" , "Participate in the calendar system"), + ("CN" , "Your common name"), + ("LANG" , "Language"), + ("TZID" , "Time zone/regime"), + ("incoming" , "How to present incoming calendar messages"), + ("freebusy_sharing" , "Share free/busy information"), + ("freebusy_bundling" , "Bundle free/busy details with messages"), + ("freebusy_publishing" , "Publish free/busy details via the Web"), + ("freebusy_messages" , "Deliver details of received free/busy messages"), + ("add_method_response" , "How to respond to messages adding events"), + ("event_refreshing" , "How to handle event refresh requests"), + ("organiser_replacement" , "Recognise whom as a new organiser of an event?"), + ] def handle_request(self): args = self.env.get_args() @@ -54,20 +51,78 @@ if not action: return ["action"] + if save: + errors = self.update_preferences() + if errors: + return errors + else: + self.redirect(self.link_to()) + + elif cancel: + self.redirect(self.link_to()) + return None + def update_preferences(self): + + "Update the stored preferences." + + settings = self.get_current_preferences() + prefs = self.get_preferences() + errors = [] + + for name, value in settings.items(): + choices = prefs.known_key_choices.get(name) + if choices and not choices.has_key(value): + errors.append(name) + + if errors: + return errors + + for name, value in settings.items(): + prefs[name] = value + + # Request logic methods. + + def is_initial_load(self): + + "Return whether the event is being loaded and shown for the first time." + + return not self.env.get_args().has_key("editing") + + def get_stored_preferences(self): + + "Return stored preference information for the current user." + + prefs = self.get_preferences() + return dict(prefs.items()) + + def get_current_preferences(self): + + "Return the preferences currently being edited." + + if self.is_initial_load(): + return self.get_stored_preferences() + else: + return dict([(name, values and values[0] or "") for (name, values) in self.env.get_args().items()]) + # Output fragment methods. def show_preferences(self, errors=None): + + "Show the preferences, indicating any 'errors' in the output." + page = self.page + settings = self.get_current_preferences() + prefs = self.get_preferences() + + # Add a hidden control to help determine whether editing has already begun. + + self.control("editing", "hidden", "true") # Show the range of preferences, getting all possible entries and using # configuration defaults. - prefs = self.get_preferences() - items = prefs.items(True, None, True) - items.sort() - page.table(class_="profile", cellspacing=5, cellpadding=5) page.thead() page.tr() @@ -76,36 +131,58 @@ page.thead.close() page.tbody() - for name, value in items: - label = self.pref_labels.get(name) - if not label: - continue + for name, label in self.pref_labels: + value = settings.get(name) + default = prefs.known_keys.get(name) + choices = prefs.known_key_choices.get(name) page.tr() page.th(class_="profileheading %s%s" % (name, errors and name in errors and " error" or "")) - page.label(label) + page.label(label, for_=name) page.th.close() page.td() - page.input(name=name, value=(value or ""), type="text", class_="preference") + + if not choices: + page.input(name=name, value=(value or default), type="text", class_="preference", id_=name) + else: + choices = list(choices.items()) + choices.sort() + self.menu(name, default, choices, [value], class_="preference") + page.td.close() page.tr.close() page.tbody.close() page.table.close() + def show_controls(self): + + "Show controls for performing actions." + + page = self.page + + page.p(class_="controls") + page.input(name="save", type="submit", value="Save") + page.input(name="cancel", type="submit", value="Cancel") + page.p.close() + # Full page output methods. def show(self): "Show the preferences of a user." + page = self.page errors = self.handle_request() if not errors: return True self.new_page(title="Profile") + page.form(method="POST") self.show_preferences(errors) + self.show_controls() + page.form.close() return True diff -r a348efd65184 -r f04e9ba8fe96 imipweb/resource.py --- a/imipweb/resource.py Sun Oct 25 01:25:29 2015 +0200 +++ b/imipweb/resource.py Sun Oct 25 18:53:50 2015 +0100 @@ -262,16 +262,17 @@ else: page.input(name=name, type=type, value=value, **kw) - def menu(self, name, default, items, class_="", index=None): + def menu(self, name, default, items, values=None, class_="", index=None): """ Show a select menu having the given 'name', set to the given 'default', - providing the given (value, label) 'items', and employing the given CSS - 'class_' if specified. + providing the given (value, label) 'items', selecting the given 'values' + (or using the request parameters if not specified), and employing the + given CSS 'class_' if specified. """ page = self.page - values = self.env.get_args().get(name, [default]) + values = values or self.env.get_args().get(name, [default]) if index is not None: values = values[index:] values = values and values[0:1] or [default]