1.1 --- a/htdocs/styles.css Sun Oct 25 01:25:29 2015 +0200
1.2 +++ b/htdocs/styles.css Sun Oct 25 18:53:50 2015 +0100
1.3 @@ -1,15 +1,29 @@
1.4 -body,
1.5 -#participants,
1.6 -#pending-requests {
1.7 +body {
1.8 background-color: #fff;
1.9 }
1.10
1.11 +#user-navigation {
1.12 + float: right;
1.13 + clear: right;
1.14 +}
1.15 +
1.16 +#user-navigation a {
1.17 + background-color: #7bf;
1.18 + color: #000;
1.19 + text-decoration: none;
1.20 + font-weight: bold;
1.21 + padding: 0.25em;
1.22 + border: 1px dotted #000;
1.23 +}
1.24 +
1.25 #participants {
1.26 float: right;
1.27 + clear: right;
1.28 }
1.29
1.30 #pending-requests {
1.31 float: left;
1.32 + clear: left;
1.33 }
1.34
1.35 #calendar-controls,
2.1 --- a/imiptools/profile.py Sun Oct 25 01:25:29 2015 +0200
2.2 +++ b/imiptools/profile.py Sun Oct 25 18:53:50 2015 +0100
2.3 @@ -24,6 +24,10 @@
2.4 from imiptools.filesys import fix_permissions, FileBase
2.5 from os.path import exists, isdir
2.6 from os import listdir, makedirs
2.7 +import pytz
2.8 +
2.9 +def identity_dict(l):
2.10 + return dict([(i, i) for i in l])
2.11
2.12 class Preferences(FileBase):
2.13
2.14 @@ -48,6 +52,51 @@
2.15 "permitted_times" : None,
2.16 }
2.17
2.18 + known_key_choices = {
2.19 + "TZID" : identity_dict(pytz.all_timezones),
2.20 + "add_method_response" : {
2.21 + "add" : "Add events",
2.22 + "ignore" : "Ignore requests",
2.23 + "refresh" : "Ask for refreshed event details"
2.24 + },
2.25 + "event_refreshing" : {
2.26 + "never" : "Do not respond",
2.27 + "always" : "Always respond"
2.28 + },
2.29 + "freebusy_bundling" : {
2.30 + "never" : "Never",
2.31 + "always" : "Always"
2.32 + },
2.33 + "freebusy_messages" : {
2.34 + "none" : "Do not notify",
2.35 + "notify" : "Notify"
2.36 + },
2.37 + "freebusy_publishing" : {
2.38 + "publish" : "Publish",
2.39 + "no" : "Do not publish"
2.40 + },
2.41 + "freebusy_sharing" : {
2.42 + "share" : "Share",
2.43 + "no" : "Do not share"
2.44 + },
2.45 + "incoming" : {
2.46 + "message-only" : "Original message only",
2.47 + "message-then-summary" : "Original message followed by a separate summary message",
2.48 + "summary-then-message" : "Summary message followed by the original message",
2.49 + "summary-only" : "Summary message only",
2.50 + "summary-wraps-message" : "Summary message wrapping the original message"
2.51 + },
2.52 + "organiser_replacement" : {
2.53 + "any" : "Anyone",
2.54 + "attendee" : "Existing attendees only",
2.55 + "never" : "Never allow organiser replacement"
2.56 + },
2.57 + "participating" : {
2.58 + "participate" : "Participate",
2.59 + "no" : "Do not participate"
2.60 + }
2.61 + }
2.62 +
2.63 def __init__(self, user, store_dir=None):
2.64 FileBase.__init__(self, store_dir or config.PREFERENCES_DIR)
2.65 self.user = user
2.66 @@ -109,6 +158,8 @@
2.67 'all_known' is set to a true value, with absent entries providing a
2.68 default of None or any indicated 'default' or, if 'config_default' is
2.69 set to a true value, the default value from the config module.
2.70 +
2.71 + Each entry will have the form (key, value).
2.72 """
2.73
2.74 l = []
2.75 @@ -116,6 +167,22 @@
2.76 l.append((key, self.get(key, default, config_default)))
2.77 return l
2.78
2.79 + def choices(self, all_known=False, default=None, config_default=False):
2.80 +
2.81 + """
2.82 + Return all entries in the preferences or all known entries if
2.83 + 'all_known' is set to a true value, with absent entries providing a
2.84 + default of None or any indicated 'default' or, if 'config_default' is
2.85 + set to a true value, the default value from the config module.
2.86 +
2.87 + Each entry will have the form (key, value, choices).
2.88 + """
2.89 +
2.90 + l = []
2.91 + for key, value in self.items(all_known, default, config_default):
2.92 + l.append((key, value, self.known_key_choices.get(key)))
2.93 + return l
2.94 +
2.95 def __getitem__(self, name):
2.96
2.97 "Return the value for 'name', raising a KeyError if absent."
3.1 --- a/imipweb/calendar.py Sun Oct 25 01:25:29 2015 +0200
3.2 +++ b/imipweb/calendar.py Sun Oct 25 18:53:50 2015 +0100
3.3 @@ -20,7 +20,7 @@
3.4 """
3.5
3.6 from datetime import datetime, timedelta
3.7 -from imiptools.data import get_address, get_uri, uri_parts
3.8 +from imiptools.data import get_address, get_uri, get_verbose_address, uri_parts
3.9 from imiptools.dates import format_datetime, get_date, get_datetime, \
3.10 get_datetime_item, get_end_of_day, get_start_of_day, \
3.11 get_start_of_next_day, get_timestamp, ends_on_same_day, \
3.12 @@ -197,6 +197,17 @@
3.13
3.14 # Page fragment methods.
3.15
3.16 + def show_user_navigation(self):
3.17 +
3.18 + "Show user-specific navigation."
3.19 +
3.20 + page = self.page
3.21 + user_attr = self.get_user_attributes()
3.22 +
3.23 + page.p(id_="user-navigation")
3.24 + page.a(get_verbose_address(self.user, user_attr), href=self.link_to("profile"), class_="username")
3.25 + page.p.close()
3.26 +
3.27 def show_requests_on_page(self):
3.28
3.29 "Show requests for the current user."
3.30 @@ -536,6 +547,7 @@
3.31
3.32 page.form(method="POST")
3.33
3.34 + self.show_user_navigation()
3.35 self.show_requests_on_page()
3.36 self.show_participants_on_page(participants)
3.37
4.1 --- a/imipweb/event.py Sun Oct 25 01:25:29 2015 +0200
4.2 +++ b/imipweb/event.py Sun Oct 25 18:53:50 2015 +0100
4.3 @@ -384,7 +384,7 @@
4.4 # Show participation status, editable for the current user.
4.5
4.6 if attendee_uri == self.user:
4.7 - self.menu("partstat", partstat, self.partstat_items, "partstat")
4.8 + self.menu("partstat", partstat, self.partstat_items, class_="partstat")
4.9
4.10 # Allow the participation indicator to act as a submit
4.11 # button in order to refresh the page and show a control for
5.1 --- a/imipweb/profile.py Sun Oct 25 01:25:29 2015 +0200
5.2 +++ b/imipweb/profile.py Sun Oct 25 18:53:50 2015 +0100
5.3 @@ -19,31 +19,28 @@
5.4 this program. If not, see <http://www.gnu.org/licenses/>.
5.5 """
5.6
5.7 -from imiptools import config
5.8 -from imipweb.resource import ResourceClient
5.9 +from imipweb.resource import FormUtilities, ResourceClient
5.10
5.11 -class ProfilePage(ResourceClient):
5.12 +class ProfilePage(ResourceClient, FormUtilities):
5.13
5.14 "A request handler for the user profile page."
5.15
5.16 - # See: imiptools.profile
5.17 + # See: imiptools.config, imiptools.profile
5.18
5.19 - pref_labels = {
5.20 - "CN" : "Common name",
5.21 - "LANG" : "Language",
5.22 - "TZID" : "Time zone/regime",
5.23 - "add_method_response" : "Respond to messages adding events with...",
5.24 - "event_refreshing" : "Handle event refresh requests automatically",
5.25 - "freebusy_bundling" : "Bundle free/busy details with messages",
5.26 - "freebusy_messages" : "Notify about received free/busy messages",
5.27 - "freebusy_offers" : "Reserve time periods when making counter-proposals",
5.28 - "freebusy_publishing" : "Publish free/busy details via the Web",
5.29 - "freebusy_sharing" : "Share free/busy information at all",
5.30 - "incoming" : "Incoming calendar messages presented using...",
5.31 - "organiser_replacement" : "Recognise which kinds of participants as replacement organisers...",
5.32 - "participating" : "Participate in the calendar system at all?",
5.33 - "permitted_times" : None,
5.34 - }
5.35 + pref_labels = [
5.36 + ("participating" , "Participate in the calendar system"),
5.37 + ("CN" , "Your common name"),
5.38 + ("LANG" , "Language"),
5.39 + ("TZID" , "Time zone/regime"),
5.40 + ("incoming" , "How to present incoming calendar messages"),
5.41 + ("freebusy_sharing" , "Share free/busy information"),
5.42 + ("freebusy_bundling" , "Bundle free/busy details with messages"),
5.43 + ("freebusy_publishing" , "Publish free/busy details via the Web"),
5.44 + ("freebusy_messages" , "Deliver details of received free/busy messages"),
5.45 + ("add_method_response" , "How to respond to messages adding events"),
5.46 + ("event_refreshing" , "How to handle event refresh requests"),
5.47 + ("organiser_replacement" , "Recognise whom as a new organiser of an event?"),
5.48 + ]
5.49
5.50 def handle_request(self):
5.51 args = self.env.get_args()
5.52 @@ -54,20 +51,78 @@
5.53 if not action:
5.54 return ["action"]
5.55
5.56 + if save:
5.57 + errors = self.update_preferences()
5.58 + if errors:
5.59 + return errors
5.60 + else:
5.61 + self.redirect(self.link_to())
5.62 +
5.63 + elif cancel:
5.64 + self.redirect(self.link_to())
5.65 +
5.66 return None
5.67
5.68 + def update_preferences(self):
5.69 +
5.70 + "Update the stored preferences."
5.71 +
5.72 + settings = self.get_current_preferences()
5.73 + prefs = self.get_preferences()
5.74 + errors = []
5.75 +
5.76 + for name, value in settings.items():
5.77 + choices = prefs.known_key_choices.get(name)
5.78 + if choices and not choices.has_key(value):
5.79 + errors.append(name)
5.80 +
5.81 + if errors:
5.82 + return errors
5.83 +
5.84 + for name, value in settings.items():
5.85 + prefs[name] = value
5.86 +
5.87 + # Request logic methods.
5.88 +
5.89 + def is_initial_load(self):
5.90 +
5.91 + "Return whether the event is being loaded and shown for the first time."
5.92 +
5.93 + return not self.env.get_args().has_key("editing")
5.94 +
5.95 + def get_stored_preferences(self):
5.96 +
5.97 + "Return stored preference information for the current user."
5.98 +
5.99 + prefs = self.get_preferences()
5.100 + return dict(prefs.items())
5.101 +
5.102 + def get_current_preferences(self):
5.103 +
5.104 + "Return the preferences currently being edited."
5.105 +
5.106 + if self.is_initial_load():
5.107 + return self.get_stored_preferences()
5.108 + else:
5.109 + return dict([(name, values and values[0] or "") for (name, values) in self.env.get_args().items()])
5.110 +
5.111 # Output fragment methods.
5.112
5.113 def show_preferences(self, errors=None):
5.114 +
5.115 + "Show the preferences, indicating any 'errors' in the output."
5.116 +
5.117 page = self.page
5.118 + settings = self.get_current_preferences()
5.119 + prefs = self.get_preferences()
5.120 +
5.121 + # Add a hidden control to help determine whether editing has already begun.
5.122 +
5.123 + self.control("editing", "hidden", "true")
5.124
5.125 # Show the range of preferences, getting all possible entries and using
5.126 # configuration defaults.
5.127
5.128 - prefs = self.get_preferences()
5.129 - items = prefs.items(True, None, True)
5.130 - items.sort()
5.131 -
5.132 page.table(class_="profile", cellspacing=5, cellpadding=5)
5.133 page.thead()
5.134 page.tr()
5.135 @@ -76,36 +131,58 @@
5.136 page.thead.close()
5.137 page.tbody()
5.138
5.139 - for name, value in items:
5.140 - label = self.pref_labels.get(name)
5.141 - if not label:
5.142 - continue
5.143 + for name, label in self.pref_labels:
5.144 + value = settings.get(name)
5.145 + default = prefs.known_keys.get(name)
5.146 + choices = prefs.known_key_choices.get(name)
5.147
5.148 page.tr()
5.149 page.th(class_="profileheading %s%s" % (name, errors and name in errors and " error" or ""))
5.150 - page.label(label)
5.151 + page.label(label, for_=name)
5.152 page.th.close()
5.153 page.td()
5.154 - page.input(name=name, value=(value or ""), type="text", class_="preference")
5.155 +
5.156 + if not choices:
5.157 + page.input(name=name, value=(value or default), type="text", class_="preference", id_=name)
5.158 + else:
5.159 + choices = list(choices.items())
5.160 + choices.sort()
5.161 + self.menu(name, default, choices, [value], class_="preference")
5.162 +
5.163 page.td.close()
5.164 page.tr.close()
5.165
5.166 page.tbody.close()
5.167 page.table.close()
5.168
5.169 + def show_controls(self):
5.170 +
5.171 + "Show controls for performing actions."
5.172 +
5.173 + page = self.page
5.174 +
5.175 + page.p(class_="controls")
5.176 + page.input(name="save", type="submit", value="Save")
5.177 + page.input(name="cancel", type="submit", value="Cancel")
5.178 + page.p.close()
5.179 +
5.180 # Full page output methods.
5.181
5.182 def show(self):
5.183
5.184 "Show the preferences of a user."
5.185
5.186 + page = self.page
5.187 errors = self.handle_request()
5.188
5.189 if not errors:
5.190 return True
5.191
5.192 self.new_page(title="Profile")
5.193 + page.form(method="POST")
5.194 self.show_preferences(errors)
5.195 + self.show_controls()
5.196 + page.form.close()
5.197
5.198 return True
5.199
6.1 --- a/imipweb/resource.py Sun Oct 25 01:25:29 2015 +0200
6.2 +++ b/imipweb/resource.py Sun Oct 25 18:53:50 2015 +0100
6.3 @@ -262,16 +262,17 @@
6.4 else:
6.5 page.input(name=name, type=type, value=value, **kw)
6.6
6.7 - def menu(self, name, default, items, class_="", index=None):
6.8 + def menu(self, name, default, items, values=None, class_="", index=None):
6.9
6.10 """
6.11 Show a select menu having the given 'name', set to the given 'default',
6.12 - providing the given (value, label) 'items', and employing the given CSS
6.13 - 'class_' if specified.
6.14 + providing the given (value, label) 'items', selecting the given 'values'
6.15 + (or using the request parameters if not specified), and employing the
6.16 + given CSS 'class_' if specified.
6.17 """
6.18
6.19 page = self.page
6.20 - values = self.env.get_args().get(name, [default])
6.21 + values = values or self.env.get_args().get(name, [default])
6.22 if index is not None:
6.23 values = values[index:]
6.24 values = values and values[0:1] or [default]