~dongpo-deng/sahana-eden/test

« back to all changes in this revision

Viewing changes to controllers/admin.py

  • Committer: Deng Dongpo
  • Date: 2010-08-01 09:29:44 UTC
  • Revision ID: dongpo@dhcp-21193.iis.sinica.edu.tw-20100801092944-8t9obt4xtl7otesb
initial

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
"""
 
4
    Admin Controllers
 
5
"""
 
6
 
 
7
import cPickle as pickle
 
8
import csv
 
9
from gluon.admin import *
 
10
from gluon.fileutils import listdir
 
11
 
 
12
module = "admin"
 
13
 
 
14
# Options Menu (available in all Functions' Views)
 
15
# - can Insert/Delete items from default menus within a function, if required.
 
16
response.menu_options = admin_menu_options
 
17
 
 
18
# S3 framework functions
 
19
def index():
 
20
    "Module's Home Page"
 
21
 
 
22
    module_name = deployment_settings.modules[module].name_nice
 
23
 
 
24
    return dict(module_name=module_name)
 
25
 
 
26
@auth.shn_requires_membership(1)
 
27
def setting():
 
28
    "RESTful CRUD controller"
 
29
 
 
30
    table = db.s3_setting
 
31
 
 
32
    table.admin_name.label = T("Admin Name")
 
33
    table.admin_email.label = T("Admin Email")
 
34
    table.admin_tel.label = T("Admin Tel")
 
35
    table.utc_offset.label = T("UTC Offset")
 
36
    table.theme.label = T("Theme")
 
37
    table.theme.comment = DIV(A(T("Add Theme"), _class="colorbox", _href=URL(r=request, c="admin", f="theme", args="create", vars=dict(format="popup")), _target="top", _title=T("Add Theme"))),
 
38
    table.debug.label = T("Debug")
 
39
    table.debug.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Debug|Switch this on to use individual CSS/Javascript files for diagnostics during development."))
 
40
    table.self_registration.label = T("Self Registration")
 
41
    table.self_registration.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Self-registration|Can users register themselves for authenticated login access?"))
 
42
    table.security_policy.label = T("Security Policy")
 
43
    table.security_policy.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Security Policy|The simple policy allows anonymous users to Read & registered users to Edit. The full security policy allows the administrator to set permissions on individual tables or records - see models/zzz.py."))
 
44
    table.archive_not_delete.label = T("Archive not Delete")
 
45
    table.archive_not_delete.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Archive not Delete|If this setting is enabled then all deleted records are just flagged as deleted instead of being really deleted. They will appear in the raw database access but won't be visible to normal users."))
 
46
    table.audit_read.label = T("Audit Read")
 
47
    table.audit_read.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Audit Read|If enabled then a log is maintained of all records a user accesses. If disabled then it can still be enabled on a per-module basis."))
 
48
    table.audit_write.label = T("Audit Write")
 
49
    table.audit_write.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Audit Write|If enabled then a log is maintained of all records a user edits. If disabled then it can still be enabled on a per-module basis."))
 
50
 
 
51
    s3.crud_strings.setting.title_update = T("Edit Settings")
 
52
    s3.crud_strings.setting.msg_record_modified = T("Settings updated")
 
53
    s3.crud_strings.setting.label_list_button = None
 
54
    #crud.settings.update_next = URL(r=request, args=[1, "update"])
 
55
 
 
56
    s3xrc.model.configure(table,
 
57
                          #onvalidation=theme_check,
 
58
                          onaccept=theme_apply)
 
59
    return shn_rest_controller("s3", "setting", deletable=False, listadd=False)
 
60
    s3xrc.model.clear_config(table, "onvalidation", "onaccept")
 
61
 
 
62
@auth.shn_requires_membership(1)
 
63
def theme():
 
64
    "RESTful CRUD controller"
 
65
    resource = "theme"
 
66
    tablename = module + "_" + resource
 
67
    table = db[tablename]
 
68
 
 
69
    # Model options
 
70
    table.name.label = T("Name")
 
71
    table.name.comment = SPAN("*", _class="req")
 
72
    #table.logo.label = T("Logo")
 
73
    #table.logo.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Logo|Name of the file (& optional sub-path) located in static which should be used for the top-left image."))
 
74
    #table.header_background.label = T("Header Background")
 
75
    #table.header_background.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Header Background|Name of the file (& optional sub-path) located in static which should be used for the background of the header."))
 
76
    #table.footer.label = T("Footer")
 
77
    #table.footer.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Footer|Name of the file (& optional sub-path) located in views which should be used for footer."))
 
78
    table.text_direction.label = T("Text Direction")
 
79
    table.text_direction.comment = A(SPAN("[Help]"), _class="tooltip", _title=T("Text Direction|Whilst most languages are read from Left-to-Right, Arabic, Hebrew & Farsi go from Right-to-Left."))
 
80
    table.col_background.label = T("Background Colour")
 
81
    table.col_txt.label = T("Text Colour for Text blocks")
 
82
    table.col_txt_background.label = T("Background Colour for Text blocks")
 
83
    table.col_txt_border.label = T("Border Colour for Text blocks")
 
84
    table.col_txt_underline.label = T("Colour for Underline of Subheadings")
 
85
    table.col_menu.label = T("Colour of dropdown menus")
 
86
    table.col_highlight.label = T("Colour of selected menu items")
 
87
    table.col_input.label = T("Colour of selected Input fields")
 
88
    table.col_border_btn_out.label = T("Colour of bottom of Buttons when not pressed")
 
89
    table.col_border_btn_in.label = T("Colour of bottom of Buttons when pressed")
 
90
    table.col_btn_hover.label = T("Colour of Buttons when hovering")
 
91
    
 
92
    # CRUD Strings
 
93
    ADD_THEME = T("Add Theme")
 
94
    LIST_THEMES = T("List Themes")
 
95
    s3.crud_strings[resource] = Storage(
 
96
        title_create = ADD_THEME,
 
97
        title_display = T("Theme Details"),
 
98
        title_list = LIST_THEMES,
 
99
        title_update = T("Edit Theme"),
 
100
        title_search = T("Search Themes"),
 
101
        subtitle_create = T("Add New Theme"),
 
102
        subtitle_list = T("Themes"),
 
103
        label_list_button = LIST_THEMES,
 
104
        label_create_button = ADD_THEME,
 
105
        msg_record_created = T("Theme added"),
 
106
        msg_record_modified = T("Theme updated"),
 
107
        msg_record_deleted = T("Theme deleted"),
 
108
        msg_list_empty = T("No Themes currently defined"))
 
109
 
 
110
    s3xrc.model.configure(table,
 
111
                          #onvalidation=theme_check,
 
112
                          #list_fields=["id", "name", "logo", "footer", "col_background"],
 
113
                          list_fields=["id", "name", "col_background"],
 
114
                          )
 
115
 
 
116
    return shn_rest_controller(module, resource)
 
117
    s3xrc.model.clear_config(table, "onvalidation")
 
118
 
 
119
def theme_apply(form):
 
120
    "Apply the Theme specified by Form"
 
121
    if form.vars.theme:
 
122
        # Valid form
 
123
        # Relevant paths
 
124
        template = os.path.join(request.folder, "static", "styles", "S3", "template.css")
 
125
        tmp_folder = os.path.join(request.folder, "static", "scripts", "tools")
 
126
        out_file = os.path.join(request.folder, "static", "styles", "S3", "sahana.css")
 
127
        out_file2 = os.path.join(request.folder, "static", "styles", "S3", "sahana.min.css")
 
128
        # Check permissions
 
129
        if not os.access(template, os.R_OK):
 
130
            session.error = T("Template file %s not readable - unable to apply theme!" % template)
 
131
            redirect(URL(r=request, args=request.args))
 
132
        if not os.access(tmp_folder, os.W_OK):
 
133
            session.error = T("Temp folder %s not writable - unable to apply theme!" % tmp_folder)
 
134
            redirect(URL(r=request, args=request.args))
 
135
        if not os.access(out_file, os.W_OK):
 
136
            session.error = T("CSS file %s not writable - unable to apply theme!" % out_file)
 
137
            redirect(URL(r=request, args=request.args))
 
138
        if not os.access(out_file2, os.W_OK):
 
139
            session.error = T("CSS file %s not writable - unable to apply theme!" % out_file2)
 
140
            redirect(URL(r=request, args=request.args))
 
141
        # Read in Template
 
142
        inpfile = open(template, "r")
 
143
        lines = inpfile.readlines()
 
144
        inpfile.close()
 
145
        # Read settings from Database
 
146
        theme = db(db.admin_theme.id == form.vars.theme).select(limitby=(0, 1)).first()
 
147
        default_theme = db(db.admin_theme.id == 1).select(limitby=(0, 1)).first()
 
148
        #if theme.logo:
 
149
        #    logo = theme.logo
 
150
        #else:
 
151
        #    logo = default_theme.logo
 
152
        #if theme.header_background:
 
153
        #    header_background = theme.header_background
 
154
        #else:
 
155
        #    header_background = default_theme.header_background
 
156
        if theme.text_direction:
 
157
            text_direction = theme.text_direction
 
158
        else:
 
159
            text_direction = "ltr"
 
160
        # Write out CSS
 
161
        ofile = open(out_file, "w")
 
162
        for line in lines:
 
163
            #line = line.replace("YOURLOGOHERE", logo)
 
164
            #line = line.replace("HEADERBACKGROUND", header_background )
 
165
            line = line.replace("TEXT_DIRECTION", text_direction)
 
166
            # Iterate through Colours
 
167
            for key in theme.keys():
 
168
                if key[:4] == "col_":
 
169
                    if theme[key]:
 
170
                        line = line.replace(key, theme[key])
 
171
                    else:
 
172
                        line = line.replace(key, default_theme[key])
 
173
            ofile.write(line)
 
174
        ofile.close()
 
175
 
 
176
        # Minify
 
177
        from subprocess import PIPE, check_call
 
178
        currentdir = os.getcwd()
 
179
        os.chdir(os.path.join(currentdir, request.folder, "static", "scripts", "tools"))
 
180
        import sys
 
181
        # If started as a Windows service, os.sys.executable is no longer python
 
182
        if ("win" in sys.platform):
 
183
            pythonpath = os.path.join(sys.prefix, "python.exe")
 
184
        else:
 
185
            pythonpath = os.sys.executable
 
186
        try:
 
187
            proc = check_call([pythonpath, "build.sahana.py", "CSS", "NOGIS"], stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=False)
 
188
        except:
 
189
            session.error = T("Error encountered while applying the theme.")
 
190
            redirect(URL(r=request, args=request.args))
 
191
        os.chdir(currentdir)
 
192
 
 
193
        # Don't do standard redirect to List view as we only want this option available
 
194
        redirect(URL(r=request, args=[1, "update"]))
 
195
    else:
 
196
        session.error = INVALIDREQUEST
 
197
        redirect(URL(r=request))
 
198
 
 
199
def theme_check(form):
 
200
    "Check the Theme has valid files available. Deprecated"
 
201
    # Check which form we're called by
 
202
    if form.vars.theme:
 
203
        # Called from Settings
 
204
        theme = db(db.admin_theme.id == form.vars.theme).select(db.admin_theme.logo, limitby=(0, 1)).first()
 
205
        logo = theme.logo
 
206
        #header_background = theme.header_background
 
207
        #footer = theme.footer
 
208
    #elif form.vars.logo and form.vars.footer:
 
209
    elif form.vars.logo:
 
210
        # Called from Theme
 
211
        logo = form.vars.logo
 
212
        #header_background = form.vars.header_background
 
213
        #footer = form.vars.footer
 
214
    else:
 
215
        session.error = INVALIDREQUEST
 
216
        redirect(URL(r=request))
 
217
 
 
218
    _logo = os.path.join(request.folder, "static", logo)
 
219
    #_header_background = os.path.join(request.folder, "static", header_background)
 
220
    #_footer = os.path.join(request.folder, "views", footer)
 
221
    if not os.access(_logo, os.R_OK):
 
222
        form.errors["logo"] = T("Logo file %s missing!" % logo)
 
223
        return
 
224
    #if not os.access(_header_background, os.R_OK):
 
225
    #    form.errors["header_background"] = T("Header background file %s missing!" % logo)
 
226
    #    return
 
227
    #if not os.access(_footer, os.R_OK):
 
228
    #    form.errors["footer"] = T("Footer file %s missing!" % footer)
 
229
    #    return
 
230
    # Validation passed
 
231
    return
 
232
 
 
233
@auth.shn_requires_membership(1)
 
234
def user():
 
235
    "RESTful CRUD controller"
 
236
    module = "auth"
 
237
    resource = "user"
 
238
    tablename = module + "_" + resource
 
239
    table = db[tablename]
 
240
 
 
241
    # Model options
 
242
 
 
243
    # CRUD Strings
 
244
    ADD_USER = T("Add User")
 
245
    LIST_USERS = T("List Users")
 
246
    s3.crud_strings[tablename] = Storage(
 
247
        title_create = ADD_USER,
 
248
        title_display = T("User Details"),
 
249
        title_list = LIST_USERS,
 
250
        title_update = T("Edit User"),
 
251
        title_search = T("Search Users"),
 
252
        subtitle_create = T("Add New User"),
 
253
        subtitle_list = T("Users"),
 
254
        label_list_button = LIST_USERS,
 
255
        label_create_button = ADD_USER,
 
256
        label_delete_button = T("Delete User"),
 
257
        msg_record_created = T("User added"),
 
258
        msg_record_modified = T("User updated"),
 
259
        msg_record_deleted = T("User deleted"),
 
260
        msg_list_empty = T("No Users currently registered"))
 
261
 
 
262
    # Add users to Person Registry & 'Authenticated' role
 
263
    crud.settings.create_onaccept = lambda form: auth.shn_register(form)
 
264
 
 
265
    # Allow the ability for admin to Disable logins
 
266
    db.auth_user.registration_key.writable = True
 
267
    db.auth_user.registration_key.readable = True
 
268
    db.auth_user.registration_key.label = T("Disabled?")
 
269
    # In Controller to allow registration to work with UUIDs - only manual edits need this setting
 
270
    db.auth_user.registration_key.requires = IS_NULL_OR(IS_IN_SET(["disabled", "pending"]))
 
271
 
 
272
    # Pre-processor
 
273
    def user_prep(jr):
 
274
        if jr.method == "update":
 
275
            # Send an email to user if their account is approved
 
276
            # (=moved from 'pending' to 'blank'(i.e. enabled))
 
277
            s3xrc.model.configure(table,
 
278
                                  onvalidation = lambda form: user_approve(form))
 
279
        return True
 
280
    response.s3.prep = user_prep
 
281
 
 
282
    # Post-processor
 
283
    def user_postp(jr, output):
 
284
        shn_action_buttons(jr)
 
285
        return output
 
286
    response.s3.postp = user_postp
 
287
 
 
288
    output = shn_rest_controller(module, resource, main="first_name")
 
289
    
 
290
    s3xrc.model.clear_config(table, "onvalidation", "onaccept")
 
291
    
 
292
    return output
 
293
 
 
294
def user_approve(form):
 
295
    "Send an email to user if their account is approved (moved from 'pending' to 'blank'(i.e. enabled))"
 
296
    if form.vars.registration_key:
 
297
        # Now non-blank
 
298
        return
 
299
    else:
 
300
        # Now blank - lookup old value
 
301
        status = db(db.auth_user.id == request.vars.id).select(db.auth_user.registration_key, limitby=(0, 1)).first().registration_key
 
302
        if status == "pending":
 
303
            # Send email to user confirming that they are now able to login
 
304
            if not auth.settings.mailer or \
 
305
                   not auth.settings.mailer.send(to=form.vars.email,
 
306
                        subject=s3.messages.confirmation_email_subject,
 
307
                        message=s3.messages.confirmation_email):
 
308
                    session.warning = auth.messages.unable_send_email
 
309
                    return
 
310
        else:
 
311
            return
 
312
 
 
313
@auth.shn_requires_membership(1)
 
314
def usergroup():
 
315
    """
 
316
    User update form with groups
 
317
    - NB This is currently unused & has no custom view
 
318
    """
 
319
    user = request.vars.user
 
320
 
 
321
    # redirect to the user list if user id is not given
 
322
    if (user == None):
 
323
        redirect(URL(r=request, f="user"))
 
324
        return
 
325
 
 
326
    # gather common variables
 
327
    data = {}
 
328
    data["user_id"] = user
 
329
    data["username"] = db.auth_user[user].first_name + " " + \
 
330
                       db.auth_user[user].last_name
 
331
    data["role"] = db.auth_group[user].role
 
332
 
 
333
    # display the standard user details
 
334
    record = db(db.auth_user.id == user).select().first()
 
335
    db.auth_user.id.readable = False
 
336
 
 
337
    # Let admin view and modify the registration key
 
338
    db.auth_user.registration_key.writable = True
 
339
    db.auth_user.registration_key.readable = True
 
340
    db.auth_user.registration_key.label = T("Disabled?")
 
341
    db.auth_user.registration_key.requires = IS_NULL_OR(IS_IN_SET(["disabled", "pending"]))
 
342
 
 
343
    form = SQLFORM(db.auth_user, record, deletable=True)
 
344
 
 
345
    # find all groups user belongs to
 
346
    query = (db.auth_membership.user_id == user)
 
347
    allgroups = db().select(db.auth_group.ALL)
 
348
    user_membership = db(query).select(db.auth_membership.ALL)
 
349
 
 
350
    # db.auth_group[row.group_id].role
 
351
    #records = SQLTABLE(db(query).select(db.auth_membership.ALL))
 
352
 
 
353
    # handle the M to M of user to group membership for display
 
354
    records = []
 
355
    for group in allgroups:
 
356
 
 
357
        user_group_count = 0
 
358
 
 
359
        for row in user_membership:
 
360
 
 
361
            if (row.group_id == group.id):
 
362
                records.append([group.role, "on", group.id])
 
363
                user_group_count += 1
 
364
 
 
365
        if (user_group_count == 0):
 
366
            # if the group does not exist currently and is enabled
 
367
            #if request.has_key(group.id):
 
368
            if (group.id == 6):
 
369
                db.auth_membership.insert(user_id = user, group_id = group.id)
 
370
                records.append([group.role, "on", group.id])
 
371
                data["heehe"] = "yes %d" % group.id
 
372
 
 
373
            records.append([group.role, "", group.id])
 
374
 
 
375
    # Update records for user details
 
376
    if form.accepts(request.vars): \
 
377
                    response.flash="User " + data["username"] + " Updated"
 
378
    elif form.errors: \
 
379
                    response.flash="There were errors in the form"
 
380
 
 
381
    # Update records for group membership details
 
382
    for key in request.vars.keys():
 
383
        data["m_" + key] = request.vars[key]
 
384
 
 
385
    return dict(data=data, records=records, form=form)
 
386
 
 
387
@auth.shn_requires_membership(1)
 
388
def group():
 
389
    "RESTful CRUD controller"
 
390
    module = "auth"
 
391
    resource = "group"
 
392
    table = module + "_" + resource
 
393
 
 
394
    # Model options
 
395
 
 
396
    # CRUD Strings
 
397
    ADD_ROLE = T("Add Role")
 
398
    LIST_ROLES = T("List Roles")
 
399
    s3.crud_strings[table] = Storage(
 
400
        title_create = ADD_ROLE,
 
401
        title_display = T("Role Details"),
 
402
        title_list = LIST_ROLES,
 
403
        title_update = T("Edit Role"),
 
404
        title_search = T("Search Roles"),
 
405
        subtitle_create = T("Add New Role"),
 
406
        subtitle_list = T("Roles"),
 
407
        label_list_button = LIST_ROLES,
 
408
        label_create_button = ADD_ROLE,
 
409
        msg_record_created = T("Role added"),
 
410
        msg_record_modified = T("Role updated"),
 
411
        msg_record_deleted = T("Role deleted"),
 
412
        msg_list_empty = T("No Roles currently defined"))
 
413
 
 
414
    return shn_rest_controller(module, resource, main="role")
 
415
 
 
416
# Unused as poor UI
 
417
@auth.shn_requires_membership(1)
 
418
def membership():
 
419
    "RESTful CRUD controller"
 
420
    module = "auth"
 
421
    resource = "membership"
 
422
    table = module + "_" + resource
 
423
 
 
424
    # Model options
 
425
 
 
426
    # CRUD Strings
 
427
    table = "auth_membership"
 
428
    ADD_MEMBERSHIP = T("Add Membership")
 
429
    LIST_MEMBERSHIPS = T("List Memberships")
 
430
    s3.crud_strings[table] = Storage(
 
431
        title_create = ADD_MEMBERSHIP,
 
432
        title_display = T("Membership Details"),
 
433
        title_list = LIST_MEMBERSHIPS,
 
434
        title_update = T("Edit Membership"),
 
435
        title_search = T("Search Memberships"),
 
436
        subtitle_create = T("Add New Membership"),
 
437
        subtitle_list = T("Memberships"),
 
438
        label_list_button = LIST_MEMBERSHIPS,
 
439
        label_create_button = ADD_MEMBERSHIP,
 
440
        msg_record_created = T("Membership added"),
 
441
        msg_record_modified = T("Membership updated"),
 
442
        msg_record_deleted = T("Membership deleted"),
 
443
        msg_list_empty = T("No Memberships currently defined"))
 
444
 
 
445
    return shn_rest_controller(module, resource, main="user_id")
 
446
 
 
447
@auth.shn_requires_membership(1)
 
448
def users():
 
449
    "List/amend which users are in a Group"
 
450
 
 
451
    try:
 
452
        group = int(request.args(0))
 
453
    except TypeError, ValueError:
 
454
        session.error = T("Need to specify a role!")
 
455
        redirect(URL(r=request, f="group"))
 
456
 
 
457
    table = db.auth_membership
 
458
    query = table.group_id == group
 
459
    title = str(T("Role")) + ": " + db.auth_group[group].role
 
460
    description = db.auth_group[group].description
 
461
    # Start building the Return
 
462
    output = dict(title=title, description=description, group=group)
 
463
 
 
464
    if auth.settings.username:
 
465
        username = "username"
 
466
    else:
 
467
        username = "email"
 
468
 
 
469
    # Audit
 
470
    crud.settings.create_onaccept = lambda form: shn_audit_create(form, module, "membership", "html")
 
471
    # Many<>Many selection (Deletable, no Quantity)
 
472
    item_list = []
 
473
    sqlrows = db(query).select()
 
474
    even = True
 
475
    for row in sqlrows:
 
476
        if even:
 
477
            theclass = "even"
 
478
            even = False
 
479
        else:
 
480
            theclass = "odd"
 
481
            even = True
 
482
        id = row.user_id
 
483
        _user = db.auth_user[id]
 
484
        item_first = _user.first_name
 
485
        item_second = _user.last_name
 
486
        item_description = _user[username]
 
487
        id_link = A(id, _href=URL(r=request, f="user", args=[id, "read"]))
 
488
        checkbox = INPUT(_type="checkbox", _value="on", _name=id, _class="remove_item")
 
489
        item_list.append(TR(TD(id_link), TD(item_first), TD(item_second), TD(item_description), TD(checkbox), _class=theclass))
 
490
 
 
491
    if auth.settings.username:
 
492
        username_label = T("Username")
 
493
    else:
 
494
        username_label = T("Email")
 
495
    table_header = THEAD(TR(TH("ID"), TH(T("First Name")), TH(T("Last Name")), TH(username_label), TH(T("Remove"))))
 
496
    table_footer = TFOOT(TR(TD(_colspan=4), TD(INPUT(_id="submit_delete_button", _type="submit", _value=T("Remove")))))
 
497
    items = DIV(FORM(TABLE(table_header, TBODY(item_list), table_footer, _id="table-container"), _name="custom", _method="post", _enctype="multipart/form-data", _action=URL(r=request, f="group_remove_users", args=[group])))
 
498
 
 
499
    subtitle = T("Users")
 
500
    crud.messages.submit_button = T("Add")
 
501
    crud.messages.record_created = T("Role Updated")
 
502
    form = crud.create(table, next=URL(r=request, args=[group]))
 
503
    addtitle = T("Add New User to Role")
 
504
    output.update(dict(subtitle=subtitle, items=items, addtitle=addtitle, form=form))
 
505
    return output
 
506
 
 
507
@auth.shn_requires_membership(1)
 
508
def group_remove_users():
 
509
    "Remove users from a group"
 
510
    if len(request.args) == 0:
 
511
        session.error = T("Need to specify a group!")
 
512
        redirect(URL(r=request, f="group"))
 
513
    group = request.args(0)
 
514
    table = db.auth_membership
 
515
    for var in request.vars:
 
516
        user = var
 
517
        query = (table.group_id == group) & (table.user_id == user)
 
518
        db(query).delete()
 
519
    # Audit
 
520
    #crud.settings.update_onaccept = lambda form: shn_audit_update(form, "membership", "html")
 
521
    session.flash = T("Users removed")
 
522
    redirect(URL(r=request, f="users", args=[group]))
 
523
 
 
524
@auth.shn_requires_membership(1)
 
525
def groups():
 
526
    "List/amend which groups a User is in"
 
527
 
 
528
    try:
 
529
        user = int(request.args(0))
 
530
    except TypeError, ValueError:
 
531
        session.error = T("Need to specify a user!")
 
532
        redirect(URL(r=request, f="user"))
 
533
 
 
534
    table = db.auth_membership
 
535
    query = table.user_id == user
 
536
    title = db.auth_user[user].first_name + " " + db.auth_user[user].last_name
 
537
    description = db.auth_user[user].email
 
538
    # Start building the Return
 
539
    output = dict(title=title, description=description, user=user)
 
540
 
 
541
    # Audit
 
542
    crud.settings.create_onaccept = lambda form: shn_audit_create(form, module, "membership", "html")
 
543
    # Many<>Many selection (Deletable, no Quantity)
 
544
    item_list = []
 
545
    sqlrows = db(query).select()
 
546
    even = True
 
547
    for row in sqlrows:
 
548
        if even:
 
549
            theclass = "even"
 
550
            even = False
 
551
        else:
 
552
            theclass = "odd"
 
553
            even = True
 
554
        id = row.group_id
 
555
        _group = db.auth_group[id]
 
556
        item_first = _group.role
 
557
        item_description = _group.description
 
558
        id_link = A(id, _href=URL(r=request, f="group", args=[id, "read"]))
 
559
        checkbox = INPUT(_type="checkbox", _value="on", _name=id, _class="remove_item")
 
560
        item_list.append(TR(TD(id_link), TD(item_first), TD(item_description), TD(checkbox), _class=theclass))
 
561
 
 
562
    table_header = THEAD(TR(TH("ID"), TH(T("Role")), TH(T("Description")), TH(T("Remove"))))
 
563
    table_footer = TFOOT(TR(TD(_colspan=3), TD(INPUT(_id="submit_delete_button", _type="submit", _value=T("Remove")))))
 
564
    items = DIV(FORM(TABLE(table_header, TBODY(item_list), table_footer, _id="table-container"), _name="custom", _method="post", _enctype="multipart/form-data", _action=URL(r=request, f="user_remove_groups", args=[user])))
 
565
 
 
566
    subtitle = T("Roles")
 
567
    crud.messages.submit_button = T("Add")
 
568
    crud.messages.record_created = T("User Updated")
 
569
    form = crud.create(table, next=URL(r=request, args=[user]))
 
570
    addtitle = T("Add New Role to User")
 
571
    output.update(dict(subtitle=subtitle, items=items, addtitle=addtitle, form=form))
 
572
    return output
 
573
 
 
574
@auth.shn_requires_membership(1)
 
575
def user_remove_groups():
 
576
    "Remove groups from a user"
 
577
    if len(request.args) == 0:
 
578
        session.error = T("Need to specify a user!")
 
579
        redirect(URL(r=request, f="user"))
 
580
    user = request.args(0)
 
581
    table = db.auth_membership
 
582
    for var in request.vars:
 
583
        group = var
 
584
        query = (table.group_id == group) & (table.user_id == user)
 
585
        db(query).delete()
 
586
    # Audit
 
587
    #crud.settings.update_onaccept = lambda form: shn_audit_update(form, "membership", "html")
 
588
    session.flash = T("Groups removed")
 
589
    redirect(URL(r=request, f="groups", args=[user]))
 
590
 
 
591
# Import Data
 
592
@auth.shn_requires_membership(1)
 
593
def import_data():
 
594
    "Import data via POST upload to CRUD controller. Old - being replaced by Sync/Importer."
 
595
    title = T("Import Data")
 
596
    crud.messages.submit_button = "Upload"
 
597
    
 
598
    # Deprecated
 
599
    import_job_form = crud.create(db.admin_import_job)
 
600
    # Tell the CRUD create form to post to a different location
 
601
    import_job_form.custom.begin.text = str(import_job_form.custom.begin).replace(
 
602
            'action=""',
 
603
            'action="%s"' % URL(r=request, f="import_job", args=["create"]))
 
604
    
 
605
    return dict(title=title,
 
606
                import_job_form=import_job_form)
 
607
 
 
608
@auth.shn_requires_membership(1)
 
609
def import_csv_data():
 
610
    "Import CSV data via POST upload to Database."
 
611
    file = request.vars.multifile.file
 
612
    try:
 
613
        # Assumes that it is a concatenation of tables
 
614
        import_csv(file)
 
615
        session.flash = T("Data uploaded")
 
616
    except Exception, e:
 
617
        session.error = T("Unable to parse CSV file!")
 
618
    redirect(URL(r=request, f="import_data"))
 
619
 
 
620
# Export Data
 
621
@auth.requires_login()
 
622
def export_data():
 
623
    "Export data via CRUD controller. Old - being replaced by Sync."
 
624
    title = T("Export Data")
 
625
    return dict(title=title)
 
626
 
 
627
@auth.shn_requires_membership(1)
 
628
def export_csv():
 
629
    "Export entire database as CSV. Old - being replaced by Sync."
 
630
    import StringIO
 
631
    output = StringIO.StringIO()
 
632
 
 
633
    db.export_to_csv_file(output)
 
634
 
 
635
    output.seek(0)
 
636
    import gluon.contenttype
 
637
    response.headers["Content-Type"] = gluon.contenttype.contenttype(".csv")
 
638
    filename = "%s_database.csv" % (request.env.server_name)
 
639
    response.headers["Content-disposition"] = "attachment; filename=%s" % filename
 
640
    return output.read()
 
641
 
 
642
    
 
643
 
 
644
# Unstructured Data Import
 
645
# Deprecated - being replaced by Importer
 
646
@auth.shn_requires_membership(1)
 
647
def import_job():
 
648
    "RESTful CRUD controller to handle 'jobs' for importing unstructured data."
 
649
    # CRUD Strings
 
650
    module = "admin"
 
651
    resource = "import_job"
 
652
    table = "%s_%s" % (module, resource)
 
653
    # This breaks!
 
654
    jr = s3xrc.request(module, resource, request, session=session)
 
655
    CREATE_NEW_IMPORT_JOB = T("Create New Import Job")
 
656
    LIST_IMPORT_JOBS = ("List Import Jobs")
 
657
    s3.crud_strings[table] = Storage(
 
658
            title_create=CREATE_NEW_IMPORT_JOB,
 
659
            title_display=T("Import Job"),
 
660
            title_list=LIST_IMPORT_JOBS,
 
661
            title_update=T("Update Import Job"),
 
662
            subtitle_create=CREATE_NEW_IMPORT_JOB,
 
663
            subtitle_list=T("Import Jobs"),
 
664
            label_list_button=LIST_IMPORT_JOBS,
 
665
            label_create_button=T("Create Import Job"),
 
666
            msg_record_created=T("Import job created"),
 
667
            msg_list_empty=T("No import jobs")
 
668
    )
 
669
 
 
670
    if len(request.args) == 0:
 
671
        return _import_job_list(jr)
 
672
    action = request.args(0)
 
673
    if action == "create":
 
674
        return _import_job_create(jr)
 
675
    if action == "update":
 
676
        return _import_job_update(jr)
 
677
 
 
678
def _import_job_list(jr):
 
679
    return shn_list(jr, listadd=False)
 
680
 
 
681
def _import_job_create(jr):
 
682
    if jr.http != "POST":
 
683
        redirect(jr.there())
 
684
 
 
685
    def process_new_file_and_redirect(form):
 
686
        filename = form.vars.source_file_newfilename
 
687
        filepath = os.path.join(request.folder, "uploads", filename)
 
688
        query = (jr.table.id == form.vars.id)
 
689
        column_map = []
 
690
        model_fields = dict(map(
 
691
                lambda x:(x.lower(), x),
 
692
                get_matchable_fields(form.vars.module, form.vars.resource)))
 
693
        try:
 
694
            reader = csv.reader(open(filepath, "r"))
 
695
            for line in reader:
 
696
                for col in line:
 
697
                    field = None
 
698
                    col_match = col.lower()
 
699
                    if col_match in model_fields:
 
700
                        # Explicit name match.
 
701
                        field = model_fields.pop(col_match)
 
702
                    if not field:
 
703
                        # Partial name patch.
 
704
                        for field_name in model_fields:
 
705
                            if col_match in field_name or field_name in col_match:
 
706
                                field = model_fields.pop(field_name)
 
707
                                break
 
708
                    column_map.append((col, field))
 
709
                break
 
710
            pickled_map = pickle.dumps(column_map, pickle.HIGHEST_PROTOCOL)
 
711
            db(query).update(column_map=pickled_map)
 
712
        except:
 
713
            db(query).update(status="failed")
 
714
        redirect(URL(r=request, f="import_job", args=[form.vars.id, "update"]))
 
715
 
 
716
    form = crud.create(jr.table, onaccept=process_new_file_and_redirect)
 
717
    response.flash = "Form errors!"
 
718
    redirect(URL(r=request, f="import_data"))
 
719
 
 
720
def _import_job_update(jr):
 
721
    if len(request.args) < 2:
 
722
        redirect(jr.there())
 
723
    id = request.args[1]
 
724
    query = (jr.table.id == id)
 
725
    job = db(query).select()
 
726
    if not job:
 
727
        raise HTTP(404, body=s3xrc.xml.json_message(False, 404, session.error))
 
728
    job = job[0]
 
729
 
 
730
    if jr.http == "GET":
 
731
        output = _import_job_update_GET(jr, job)
 
732
    elif jr.http == "POST":
 
733
        output = _import_job_update_POST(jr, job)
 
734
    else:
 
735
        raise HTTP(400, body=INVALIDREQUEST)
 
736
 
 
737
    title = "%s - %s" % (T("Input Job"), job.description)
 
738
    output.update(dict(title=title, job=job))
 
739
    response.view = "admin/import_job_update.html"
 
740
    return output
 
741
 
 
742
def _import_job_update_GET(jr, job):
 
743
    table = db.admin_import_line
 
744
    query = (table.import_job == job.id)
 
745
 
 
746
    if job.status == "new":
 
747
        try:
 
748
            job.column_map = pickle.loads(job.column_map)
 
749
        except pickle.UnpicklingError:
 
750
            job.column_map = []
 
751
        model_fields = get_matchable_fields(job.module, job.resource)
 
752
        return dict(model_fields=model_fields)
 
753
 
 
754
    if job.status == "processing":
 
755
        num_lines = db(query).count()
 
756
        return dict(num_lines=num_lines, update_speed=60)
 
757
 
 
758
    if job.status in ["processed", "failed", "imported"]:
 
759
        def _include_field(f):
 
760
            if f in ["import_job"]:
 
761
                return False
 
762
            return table[f].readable
 
763
 
 
764
        fields = [table[f] for f in table.fields if _include_field(f)]
 
765
        headers = dict(map(lambda f: (str(f), f.label), fields))
 
766
        items = crud.select(
 
767
                db["admin_import_line"],
 
768
                query=query,
 
769
                fields=fields,
 
770
                headers=headers,
 
771
                truncate=48, _id="list", _class="display")
 
772
        response.extra_styles = ["admin/import_job.css"]
 
773
        return dict(items=items)
 
774
 
 
775
    if job.status == "import":
 
776
        query = (table.import_job == job.id) & (table.status == "imported")
 
777
        num_lines = db(query).count()
 
778
        return dict(num_lines=num_lines, update_speed=60)
 
779
 
 
780
    return {}
 
781
 
 
782
def _import_job_update_POST(jr, job):
 
783
    if job.status == "new":
 
784
        # Update column map.
 
785
        try:
 
786
            column_map = pickle.loads(job.column_map)
 
787
        except pickle.UnpicklingError:
 
788
            column_map = []
 
789
        for key, value in request.vars.iteritems():
 
790
            if not key.startswith("column_map_"):
 
791
                continue
 
792
            try:
 
793
                idx = int(key.split("_")[-1])
 
794
            except ValueError:
 
795
                continue
 
796
            if value != "None (Ignore)":
 
797
                column_map[idx] = (column_map[idx][0], value)
 
798
            else:
 
799
                column_map[idx] = (column_map[idx][0], None)
 
800
        jr.table[job.id] = dict(
 
801
                column_map=pickle.dumps(column_map, pickle.HIGHEST_PROTOCOL),
 
802
                status="processing")
 
803
        job.status = "processing"
 
804
 
 
805
    if job.status == "processed":
 
806
        query = db.admin_import_line.id
 
807
        for var, status in request.vars.iteritems():
 
808
            if not var.startswith("status_"):
 
809
                continue
 
810
            try:
 
811
                import_line_id = int(var.split("_")[-1])
 
812
            except ValueError:
 
813
                continue
 
814
            db(db.admin_import_line.id == import_line_id).update(status=status)
 
815
        jr.table[job.id] = dict(status="import")
 
816
        job.status = "import"
 
817
 
 
818
    return _import_job_update_GET(jr, job)
 
819
 
 
820
def get_matchable_fields(module, resource):
 
821
    model_fields = getattr(db, "%s_%s" % (module, resource)).fields
 
822
    for name in ["created_on", "modified_on", "id", "uuid", "deleted"]:
 
823
        model_fields.remove(name)
 
824
    model_fields.insert(0, "None (Ignore)")
 
825
    return model_fields
 
826
 
 
827
 
 
828
# Functional Testing
 
829
def handleResults():
 
830
    """Process the POST data returned from Selenium TestRunner.
 
831
    The data is written out to 2 files.  The overall results are written to
 
832
    date-time-browserName-metadata.txt as a list of key: value, one per line.  The
 
833
    suiteTable and testTables are written to date-time-browserName-results.html.
 
834
    """
 
835
 
 
836
    if not request.vars.result:
 
837
        # No results
 
838
        return
 
839
 
 
840
    # Read in results
 
841
    result = request.vars.result
 
842
    totalTime = request.vars.totalTime
 
843
    numberOfSuccesses = request.vars.numTestPasses
 
844
    numberOfFailures = request.vars.numTestFailures
 
845
    numberOfCommandSuccesses = request.vars.numCommandPasses
 
846
    numberOfCommandFailures = request.vars.numCommandFailures
 
847
    numberOfCommandErrors = request.vars.numCommandErrors
 
848
 
 
849
    suiteTable = ""
 
850
    if request.vars.suite:
 
851
        suiteTable = request.vars.suite
 
852
 
 
853
    testTables = []
 
854
    testTableNum = 1
 
855
    while request.vars["testTable.%s" % testTableNum]:
 
856
        testTable = request.vars["testTable.%s" % testTableNum]
 
857
        testTables.append(testTable)
 
858
        testTableNum += 1
 
859
        try:
 
860
            request.vars["testTable.%s" % testTableNum]
 
861
            pass
 
862
        except:
 
863
            break
 
864
 
 
865
    # Unescape the HTML tables
 
866
    import urllib
 
867
    suiteTable = urllib.unquote(suiteTable)
 
868
    testTables = map(urllib.unquote, testTables)
 
869
 
 
870
    # We want to store results separately for each browser
 
871
    browserName = getBrowserName(request.env.http_user_agent)
 
872
    date = str(request.utcnow)[:-16]
 
873
    time = str(request.utcnow)[11:-10]
 
874
    time = time.replace(":", "-")
 
875
 
 
876
    # Write out results
 
877
    outputDir = os.path.join(request.folder, "static", "selenium", "results")
 
878
    metadataFile = "%s-%s-%s-metadata.txt" % (date, time, browserName)
 
879
    dataFile = "%s-%s-%s-results.html" % (date, time, browserName)
 
880
 
 
881
    #xmlText = '<selenium result="' + result + '" totalTime="' + totalTime + '" successes="' + numberOfCommandSuccesses + '" failures="' + numberOfCommandFailures + '" errors="' + numberOfCommandErrors + '" />'
 
882
    f = open(os.path.join(outputDir, metadataFile), "w")
 
883
    for key in request.vars.keys():
 
884
        if "testTable" in key or key in ["log", "suite"]:
 
885
            pass
 
886
        else:
 
887
            print >> f, "%s: %s" % (key, request.vars[key])
 
888
    f.close()
 
889
 
 
890
    f = open(os.path.join(outputDir, dataFile), "w")
 
891
    print >> f, suiteTable
 
892
    for testTable in testTables:
 
893
        print >> f, "<br/><br/>"
 
894
        print >> f, testTable
 
895
    f.close()
 
896
 
 
897
    message = DIV(P("Results have been successfully posted to the server here:"),
 
898
        P(A(metadataFile, _href=URL(r=request, c="static", f="selenium", args=["results", metadataFile]))),
 
899
        P(A(dataFile, _href=URL(r=request, c="static", f="selenium", args=["results", dataFile]))))
 
900
 
 
901
    response.view = "display.html"
 
902
    title = T("Test Results")
 
903
    return dict(title=title, item=message)
 
904
 
 
905
# Ticket Viewer functions Borrowed from admin application of web2py
 
906
@auth.shn_requires_membership(1)
 
907
def errors():
 
908
    """ Error handler """
 
909
 
 
910
    app = request.application
 
911
 
 
912
    for item in request.vars:
 
913
        if item[:7] == "delete_":
 
914
            os.unlink(apath("%s/errors/%s" % (app, item[7:]), r=request))
 
915
 
 
916
    func = lambda p: os.stat(apath("%s/errors/%s" % (app, p), r=request)).st_mtime
 
917
    tickets = sorted(listdir(apath("%s/errors/" % app, r=request), "^\w.*"),
 
918
                     key=func,
 
919
                     reverse=True)
 
920
 
 
921
    return dict(app=app, tickets=tickets)
 
922
 
 
923
def make_link(path):
 
924
    """ Create a link from a path """
 
925
    tryFile = path.replace("\\", "/")
 
926
 
 
927
    if os.path.isabs(tryFile) and os.path.isfile(tryFile):
 
928
        (folder, filename) = os.path.split(tryFile)
 
929
        (base, ext) = os.path.splitext(filename)
 
930
        app = request.args[0]
 
931
 
 
932
        editable = {"controllers": ".py", "models": ".py", "views": ".html"}
 
933
        for key in editable.keys():
 
934
            check_extension = folder.endswith("%s/%s" % (app, key))
 
935
            if ext.lower() == editable[key] and check_extension:
 
936
                return A('"' + tryFile + '"',
 
937
                         _href=URL(r=request,
 
938
                         f="edit/%s/%s/%s" % (app, key, filename))).xml()
 
939
    return ""
 
940
 
 
941
 
 
942
def make_links(traceback):
 
943
    """ Make links using the given traceback """
 
944
 
 
945
    lwords = traceback.split('"')
 
946
 
 
947
    # Make the short circuit compatible with <= python2.4
 
948
    result = (len(lwords) != 0) and lwords[0] or ""
 
949
 
 
950
    i = 1
 
951
 
 
952
    while i < len(lwords):
 
953
        link = make_link(lwords[i])
 
954
 
 
955
        if link == "":
 
956
            result += '"' + lwords[i]
 
957
        else:
 
958
            result += link
 
959
 
 
960
            if i + 1 < len(lwords):
 
961
                result += lwords[i + 1]
 
962
                i = i + 1
 
963
 
 
964
        i = i + 1
 
965
 
 
966
    return result
 
967
 
 
968
 
 
969
class TRACEBACK(object):
 
970
    """ Generate the traceback """
 
971
 
 
972
    def __init__(self, text):
 
973
        """ TRACEBACK constructor """
 
974
 
 
975
        self.s = make_links(CODE(text).xml())
 
976
 
 
977
    def xml(self):
 
978
        """ Returns the xml """
 
979
 
 
980
        return self.s
 
981
 
 
982
 
 
983
# Ticket viewing
 
984
@auth.shn_requires_membership(1)
 
985
def ticket():
 
986
    """ Ticket handler """
 
987
 
 
988
    if len(request.args) != 2:
 
989
        session.flash = T("Invalid ticket")
 
990
        redirect(URL(r=request))
 
991
 
 
992
    app = request.args[0]
 
993
    ticket = request.args[1]
 
994
    e = RestrictedError()
 
995
    e.load(request, app, ticket)
 
996
 
 
997
    return dict(app=app,
 
998
                ticket=ticket,
 
999
                traceback=TRACEBACK(e.traceback),
 
1000
                code=e.code,
 
1001
                layer=e.layer)
 
1002