#!/bin/env python
# -*- coding: utf-8 -*-
# TOMUSS: The Online Multi User Simple Spreadsheet
# Copyright (C) 2008-2014 Thierry EXCOFFIER, Universite Claude Bernard
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Contact: Thierry.EXCOFFIER@bat710.univ-lyon1.fr
import os
import time
import threading
import socket
import re
import collections
import cgi
from . import utilities
from . import configuration
from . import column
from . import data
from . import files
from . import inscrits
from .cell import CellValue, Lines, cellempty
from . import sender
from . import plugins
def check_table(t):
cols = tuple(t.lines.lines.values())
if cols:
nr_cols = len(tuple(t.lines.lines.values())[0])
else:
return # can't check because there is no lines...
warn('%s, %s, %s' % (t.ue, len(t.columns), nr_cols))
if len(t.columns) != nr_cols:
raise ValueError('bad table')
def date_time(date):
if date is None:
return '19700101010101'
else:
return time.strptime(date, '%Y%m%d%H%M%S')
warn = utilities.warn
tables = {}
the_head = files.add('FILES','head.html')
js = utilities.js
canceled_loads = []
# Key : student_id, give the tables ue name
# Used only in 'suivi' because it does not contain year/semester
tables_of_student = {}
class Page(object):
def __init__(self, the_ticket, user_name, page_id, ttable,
user_ip, user_browser, date=None):
self.ticket = the_ticket
self.user_name = user_name
self.page_id = page_id
self.table = ttable
self.request = 0
self.browser_file = None
self.user_ip = user_ip
self.user_browser = user_browser
self.date = date
def check_identity(self, the_ticket, user_name, user_ip, user_browser):
# Allow IP change with the same identity
if (the_ticket == self.ticket
and user_name == self.user_name
and user_browser == self.user_browser):
return
warn('received ticket=%s user_name=%s user_browser=%s ip=%s' % (
the_ticket, user_name, user_browser, user_ip), what="Info")
warn('current ticket=%s user_name=%s user_browser=%s ip=%s' % (
self.ticket, self.user_name, self.user_browser, self.user_ip),
what="Info")
if configuration.regtest_sync or not configuration.regtest:
raise ValueError('Cheater')
def add_request(self, request, action, path, output_file):
request_list.append((self.page_id, request, self, action, path,
output_file))
def __str__(self):
return '%s[%s]:%s(%s)' % (self.table, self.page_id, self.user_name,
self.date)
def date_time(self):
return date_time(self.date)
def day(self):
return self.date[6:8] + '/' + self.date[4:6] + '/' + self.date[:4]
def backtrace_html(self):
return (cgi.escape(str(self)) + '<br>'
+ cgi.escape(self.user_browser) + '<br>'
+ self.user_ip)
def table_filename(year, semester, ue):
return os.path.join(configuration.db, 'Y'+str(year), 'S'+semester, ue + '.py')
def filter_language(language):
# Remove not translated languages and duplicates
t = []
for x in language.strip(",").split(','):
if x in plugins.languages:
if x not in t:
t.append(x)
return ','.join(t)
def get_preferences(user_name, create_pref=True, the_ticket=None):
my_identity2 = utilities.login_to_module(user_name)
prefs_table = table(0, 'Preferences', my_identity2, None, None,
create=create_pref)
if prefs_table is None:
p = {}
else:
p = prefs_table.template.preferences(prefs_table)
warn('Language in preferences: (%s)' % p.get('language', ''), what="lang")
if p.get('language', '') == '':
if the_ticket is None:
warn('Search old ticket to find language')
for the_ticket in ticket.tickets.values():
if the_ticket.user_name == user_name:
break
warn('Language in ticket: (%s)' % the_ticket.language, what="lang")
p['language'] = the_ticket.language
p['language'] = filter_language(p['language'])
warn('Language after filtering: ' + p['language'])
if p['language'] == '':
warn('Language in server: (%s)' % configuration.language,
what="lang")
p['language'] = configuration.language
return p
#REDEFINE
# This function returns javascript code to be included
# in the header of the 'ue' table
def table_head_more(dummy_ue):
return ''
class TT:
def create(self, ttable):
ttable.new_page('', data.ro_user, '', '')
ttable.table_attr(ttable.pages[0],'masters',[ttable.user])
empty_template = TT()
def translations_init(language):
languages = []
language = filter_language(language + ',' + configuration.language)
for lang in language.split(','):
languages.append(
'<script onload="this.onloadDone=true;" src="%s/%s.js"></script>'
% (configuration.url_files, lang))
return ('<script>var translations = {},'
+ 'preferences={"language":%s} ; </script>\n'
% js(language)
+ '\n'.join(languages) + '\n')
def table_head(year=None, semester=None, the_ticket=None,
user_name='', page_id=-1, ue='',
create_pref=True,
attrs_from=0, hide_more=False, table=None):
s = configuration.suivi.url(year, semester, the_ticket)
t = ticket.tickets.get(the_ticket, None)
prefs_table = get_preferences(user_name, create_pref, the_ticket=t)
try:
background = configuration.semesters_color[configuration.semesters.index(semester)]
background = '<style>BODY, TABLE INPUT, #current_input, BODY TABLE.colored TD { background-color: ' + background + '}</style>'
except ValueError:
background = ''
my_identity2 = utilities.login_to_module(user_name)
return (str(the_head) + background +
translations_init(prefs_table['language']) +
'<script>\n' +
'page_id = "%d" ;\n' % page_id +
'my_identity = %s ;\n' % repr(user_name) +
'my_identity2 = %s ;\n' % repr(my_identity2) +
'url = %s ;\n' % js(configuration.server_url) +
'year = "%s" ;\n' % year +
'semester = "%s" ;\n' % semester +
'ticket = "%s" ;\n' % the_ticket +
'ue = "%s" ;\n' % ue +
'suivi = %s ;\n' % js(s) +
'version = "%s" ;\n' % configuration.version +
'root = %s ;\n' % js(list(configuration.root)) +
'cas_url = %s ;\n' % repr(configuration.cas) +
'preferences = %s ;\n' % prefs_table +
'lines = {};\n' +
'columns = [];\n' +
'lines_to_load = 0 ;\n' +
'ticket_time_to_live = %d ;\n' % configuration.ticket_time_to_live +
'max_visibility_date = %d ;\n' % configuration.max_visibility_date +
'gui_record = %d ;\n' % int(configuration.gui_record) +
'check_down_connections_interval = %d ;\n' % configuration.check_down_connections_interval +
'table_attr = {\n' +
',\n'.join(attr.name+':'+js(
getattr(attrs_from, attr.name,
attr.get_default_value(table)))
for attr in column.TableAttr.attrs.values()
) + '} ;\n' +
(hide_more and '' or table_head_more(ue)) +
'</script>\n')
def import_template(names):
for name in names:
if configuration.regtest and name[0] == 'LOCAL':
continue
filename = os.path.join(*name) + '.py'
if os.path.exists(filename):
return utilities.import_reload(filename)[0]
return None
class Table(object):
new_abjs = None
force_update = 0
def __init__(self, year, semester, ue, ro=True, user=None):
warn('%s/%s/%s ro=%s user=%s' % (year, semester, ue, ro, user),
what="table")
self.year = year
self.semester = semester
self.ue = ue
self.loading = False # The table is not being loaded
self.sent_to_browsers = [] # Nothing has been sent
if self.ue == 'abj' or self.ue == 'data':
# Break the import in the file
raise ValueError("Invalid table name: '%s'" % self.ue)
# Remove -1 -2 -3 at the end of the UE name
if len(ue) > 4 and ue[-1].isdigit() and ue[-2] == '-':
self.ue_code = ue[:-2]
elif (len(ue) > 5
and ue[-1].isdigit() and ue[-2].isdigit() and ue[-3] == '-'):
self.ue_code = ue[:-3]
else:
self.ue_code = ue
for attr in column.TableAttr.attrs.values():
d = attr.get_default_value(self)
if isinstance(d, list):
d = list(d)
elif isinstance(d, dict):
d = dict(d)
setattr(self, attr.name, d)
self.pages = []
self.active_pages = []
self.columns = column.Columns(self)
self.lines = Lines(self.columns)
self.the_lock = threading.Lock()
self.ro = ro
self.mtime = 0
self.the_key_dict = collections.defaultdict(list)
self.unloaded = False
self.do_not_unload = []
dirname = os.path.join(configuration.db,
'Y'+str(self.year), 'S'+self.semester)
self.filename = os.path.join(dirname, ue + '.py')
self.is_extended = os.path.islink(self.filename)
self.template = import_template((
('LOCAL', 'LOCAL_TEMPLATES', self.ue_code),
('TEMPLATES', self.ue_code),
('LOCAL', 'LOCAL_TEMPLATES', self.semester),
('TEMPLATES', self.semester),
))
if self.template is None:
self.template = empty_template
if hasattr(self.template, 'init'):
self.template.init(self)
for v in self.template.__dict__.values():
if isinstance(v, utilities.Variables):
self.group = v._group
created = False
self.on_disc = True
if self.modifiable:
utilities.mkpath_safe(dirname)
if not os.path.exists(self.filename):
s = ['# -*- coding: utf8 -*- ']
s.append('from TOMUSS.data import *')
s = '\n'.join(s) + '\n'
utilities.mkpath_safe(dirname)
utilities.append_file_safe(self.filename, s)
created = True
else:
if not os.path.exists(self.filename):
self.on_disc = False
# Remove final .py
self.module = 'TOMUSS.' + self.filename[:-3].replace(os.path.sep,'.')
if not os.path.exists(self.filename):
# Read only table and file does not exists
return
self.mtime = os.path.getmtime(self.filename)
if not created:
data.begin(self)
try:
__import__(self.module)
utilities.unload_module(self.module)
finally:
data.end()
else:
if ro:
return # Do not create the table
# Remove this file because it is created the same second
# than the .py file, So it is not recompiled in some case.
# os.remove(self.filename + 'c')
warn('Create start', what='table')
self.user = user # Usable by template.create()
# We lock because table change methods expects to be locked.
# But this lock does not protect anything
self.lock()
try:
if hasattr(self.template, "create"):
self.template.create(self)
else:
empty_template.create(self)
finally:
self.unlock()
warn('Unlock', what='table')
warn('Create end', what='table')
self.pages[0].request = 1 # Not an empty page, it's for the stats
if self.template and hasattr(self.template, 'onload'):
self.template.onload(self)
if not ro:
# Check mails or new students
# Only mail if allow_modification is False
warn('Update student list', what='table')
update_students.append(self)
if ro and self.official_ue and not configuration.index_are_computed:
# Student are never removed, but it is not important
for login in self.the_keys():
if login in tables_of_student:
if self.ue not in tables_of_student[login]:
tables_of_student[login].add(self.ue)
else:
tables_of_student[login] = set((self.ue,))
if self.is_extended:
self.destination_is_modifiable = self.modifiable
# To forbid the edit of the same table with 2 names
self.modifiable = 0
def update(self):
"""Update the table if the file on disc changed.
It is used only by 'suivi' servers
"""
# warn('Start ro=%s dt=%f p=%s' % (self.ro, time.time() - self.mtime, os.path.exists(self.filename)), what="table")
if not self.ro:
return self
if not os.path.exists(self.filename):
# Do not work (to to recreate it)
# tables_manage("del", self.year, self.semester, self.ue)
# Should return None, but as it is very intrusive
# Make a non dangerous kludge
self.official_ue = False
return self
if time.time() - self.mtime < configuration.maximum_out_of_date:
return self
if os.path.getmtime(self.filename) <= self.mtime:
return self
warn('%s time=%s mtime=%s' % (self.ue, time.time(), self.mtime),
what="table")
return tables_manage("replace", self.year, self.semester, self.ue,
new_table = Table(self.year, self.semester,
self.ue, ro=True))
def change_mails(self, mails):
self.table_attr_computed('mails', mails)
def change_portails(self, portails):
self.table_attr_computed('portails', portails)
def table_attr_computed(self, attr, value):
if getattr(self, attr) == value:
return
setattr(self, attr, value)
t = '<script>Xtable_attr(%s,%s);</script>\n' % (repr(attr), js(value))
self.send_update(None, t)
def update_mail(self, login, mail):
warn('update mail of ' + self.location(), what="table")
self.mails[login] = mail
self.send_update(None,
('<script>update_mail(' + repr(login)
+ ',' + js(mail) + ');</script>\n').encode('utf-8')
)
def update_portail(self, login, portail):
self.portails[login] = portail
self.send_update(None,
('<script>update_portail(' + repr(login)
+ ',' + js(portail) + ');</script>\n').encode('utf-8')
)
def lock(self):
warn('start', what='debug')
sender.send_live_status('<script>b("%d/%s/%s");</script>\n' %
(self.year, self.semester, self.ue))
self.the_lock.acquire()
warn('acquired', what='debug')
def unlock(self):
warn('start', what='debug')
self.the_lock.release()
sender.send_live_status('<script>r("%d/%s/%s");</script>\n' %
(self.year, self.semester, self.ue))
def panic(self, txt):
utilities.send_backtrace('UE: %s ' % self.ue + txt,
exception=False)
self.send_update(None, "<script>window.close();</script>\n")
raise ValueError('PANIC')
def state_is_fine(self):
if self.unloaded:
self.panic('Modification on an unloaded table')
if not self.the_lock.locked():
self.panic('Modification on an unlocked table')
if self.is_extended:
self.panic('Modification on an extended table')
if configuration.read_only:
self.panic('Modification in a readonly process')
def log(self, text):
self.state_is_fine()
utilities.append_file_safe(self.filename, text + '\n')
warn(self.filename + ' ' + text)
self.mtime = time.time()
def new_page(self, ticket, user_name, user_ip, user_browser, date=None):
if not self.loading and self.modifiable:
self.log('new_page(%s ,%s, %s, %s, %s) # %d' % (
repr(ticket),
repr(user_name),
repr(user_ip),
repr(user_browser),
repr(date),
len(self.pages),
))
p = Page(ticket, user_name, len(self.pages), self,
user_ip, user_browser, date)
p.logged = self.loading or self.modifiable
p.index = len(self.sent_to_browsers) - 1
self.pages.append(p)
return p
def active_page(self, page, ffile):
warn('page=%s file=%s(%s)' % (page, ffile, ffile.closed), what="table")
page.browser_file = ffile
if page not in self.active_pages:
self.active_pages.append(page)
def remove_active_page(self, page):
self.active_pages.remove(page)
page.index = getattr(page.browser_file, 'index', page.index)
page.browser_file = None # stop a memory leak
def send_update(self, page, value, store=True):
warn('actives: %s' % str(self.active_pages), what='table')
if store:
self.sent_to_browsers.append(value)
for p in tuple(self.active_pages):
if p == page:
continue
if p.browser_file.closed:
self.remove_active_page(p)
if not hasattr(p, 'end_of_load'):
global canceled_loads
# Update list of canceled page load
now = time.time()
canceled_loads.append((now, p.user_name))
old = now - 3600
canceled_loads = [i for i in canceled_loads
if i[0] > old]
user = p.user_name
nr = list([i for i in canceled_loads if i[1] == user])
if len(nr) == 4: # 4 cancels in less than one hour
user_mail = inscrits.L_fast.mail(user)
if user_mail:
utilities.send_mail_in_background(
user_mail,
# XXX need translation
utilities._("MSG_document_problems"),
unicode(utilities.read_file(
os.path.join('FILES', 'mail_cancel')),
'utf8').encode('latin1')
)
# Send a mail to the maintainer
utilities.send_backtrace(
repr(nr),
utilities._("MSG_document_canceled") + user,
exception=False)
continue
if value is True:
sender.append(p.browser_file, self.content(p),
index=len(self.sent_to_browsers))
else:
sender.append(p.browser_file, value,
index=len(self.sent_to_browsers))
def authorized(self, user_name, value):
# Authorized because the test have yet be done in the past
if self.loading:
return True
# Values setted by user '' are modifiable
if value.author == data.rw_user:
return True
# Empty values are modifiable by anyone
if value.value == '':
return True
# The user can change its values
if value.author == user_name:
return True
# RO and RW users have all the rights
if user_name == data.ro_user or user_name == data.rw_user:
return True
# Values setted by user '*' are not modifiable
if value.author == data.ro_user:
return False
# The masters of the UE may change any value setted by another user
if user_name in self.masters:
return True
return False
def authorized_column(self, user_name, a_column):
return self.authorized(
user_name, CellValue(a_column.title, a_column.author))
def cell_change(self, page, col, lin, value=None,
date=None, force_update=False,
change_author=True):
if not self.loading and not self.modifiable:
return self.bad_ro(page)
a_column = self.columns.from_id(col)
if a_column == None:
raise ValueError(utilities._("MSG_document_bug_column") % col)
on_a_new_line = lin not in self.lines
line = self.lines[lin]
cell = line[a_column.data_col]
if value is None:
value = cell.value
if not self.loading and not force_update:
if not self.authorized(page.user_name, cell):
utilities.warn('cell value = (%s)' % cell.value)
return self.bad_auth(page, "cell_change %s/%s/%s" % (
col, lin, value))
if a_column.locked and page.user_name != data.ro_user:
self.error(page, utilities._("MSG_document_column_locked"))
return 'bad.png'
old_value = str(cell.value)
new_value = str(value)
# If the old value is the float 10
# And the new one the string 10
# The string compare will be false because "10.0" != "10"
# So we also compare non string values.
try:
equal = float(cell.value) == float(value)
except ValueError:
equal = old_value == new_value
if equal:
if cell.author == page.user_name:
return 'ok.png'
if not change_author:
return 'ok.png'
# if isinstance(value, str) and value.find('.') != -1:
if a_column.type.name == 'Note':
try:
value = float(value)
except ValueError:
pass
if (not self.loading
and a_column.repetition
and value != ''
and not equal
):
n = 0
data_col = a_column.data_col
if a_column.repetition > 0:
verify_lines = self.lines.values()
else:
grp = line[self.columns.get_grp()].value
seq = line[self.columns.get_seq()].value
verify_lines = list(self.columns.table.lines_of_grp(grp, seq))
for a_line in verify_lines:
if a_line[data_col].value == value:
n += 1
if n >= abs(a_column.repetition):
sender.append(
page.browser_file,
'<script>alert(_("ALERT_repetition")+%s+"/"+%s);</script>\n'
% (js(value), js(a_column.title)))
return 'bad.png'
if (not self.loading and self.template
and not equal
and hasattr(self.template, 'cell_change')):
self.template.cell_change(self, page, col, lin, value, date)
if a_column.data_col == 0:
login = utilities.the_login(old_value)
if login in self.the_key_dict:
try:
self.the_key_dict[login].remove(lin)
if len(self.the_key_dict[login]) == 0:
del self.the_key_dict[login]
except:
if login:
utilities.warn(str(page) + ' old_login=' + login
+ ' new_login=' + new_value
+ ' lin=' + lin
+ ' ' + repr(self.the_key_dict[login]))
new_login = utilities.the_login(new_value)
self.the_key_dict[new_login].append(lin)
if not self.loading and self.official_ue:
indexes_to_update.append((self, login, new_login))
elif on_a_new_line:
self.the_key_dict[''].append(lin)
# The class may change on value change
cell = line[a_column.data_col] = cell.set_value(value=value,
author=page.user_name,
date=date)
if not self.loading:
self.log('cell_change(%s,%s,%s,%s,"%s")' % (
page.page_id,
repr(col),
repr(lin),
repr(value),
cell.date))
t = '<script>Xcell_change(%s,%s,%s,%s,%s,%s);</script>\n' % (
js(col),
js(lin),
js(value),
js(cell.date),
js(cell.author),
js(cell.history),
)
if force_update:
self.send_update(None, t)
else:
self.send_update(page, t)
cell_changed_list_fast.append((self, lin, a_column))
else:
page.request += 1
return 'ok.png'
def default_nr_columns_change(self, n):
"""Deprecated"""
self.default_nr_columns = n
def add_master(self, name, page_id=0):
"""Deprecated"""
name = name.lower()
if name in self.masters:
self.masters.remove(name)
else:
self.masters.append(name)
if not self.loading:
# For old TEMPLATES files
self.log('table_attr("masters",%d,%s)' % (page_id, repr(name)))
def get_a_master_page(self):
"""Returns a page modifiable by the masters.
Create one if there is none"""
rw = None
for p in self.pages:
if p.user_name in self.masters:
return p
if p.user_name == data.rw_user:
rw = p
if self.masters:
return self.new_page('', self.masters[0], '', '')
else:
if rw:
return rw
return self.new_page('', data.rw_user, '', '')
def private_toggle(self, page):
"""Deprecated"""
self.private = 1 - self.private
if not self.loading: # compatibility with old TEMPLATES
self.log('table_attr("private",%d,%d)' % (page.page_id,
self.private))
return 'ok.png'
def error(self, page, message, more_in_mail=""):
utilities.send_backtrace(
'UE: %s, Page: %s' % (self.ue, page)
+ '\n' + more_in_mail, subject='###' + message,
exception=False)
if '_(' not in message:
# The message is not javascript program
message = js(message)
message = message + '+"\\n\\n"+_("ERROR_server_bug")'
sender.append(page.browser_file,
'<script>alert(%s);</script>\n' % message)
return "bad.png"
def bad_column(self, page):
return self.error(page, '_("ALERT_column_not_exist")')
def bad_auth(self, page, more_in_mail=""):
return self.error(page, '_("ALERT_not_authorized")', more_in_mail)
def bad_ro(self, page):
return self.error(page, '_("ALERT_value_ro")')
def comment_change(self, page, col, lin, value):
if not self.loading and not self.modifiable:
return self.bad_ro(page)
a_column = self.columns.from_id(col)
if a_column == None:
return self.bad_column(page)
line = self.lines[lin]
if value == line[a_column.data_col].comment:
return 'ok.png'
if not self.loading:
if not self.authorized(page.user_name,
line[a_column.data_col]):
return self.bad_auth(page, "comment_change %s/%s/%s" % (
col, lin, value))
if hasattr(self.template, 'comment_change'):
self.template.comment_change(self, page, col, lin, value)
self.log('comment_change(%s,%s,%s,%s)' % (
page.page_id,
repr(col),
repr(lin),
repr(value),
))
t = '<script>Xcomment_change(%s,%s,%s,%s);</script>\n' % (
js(page.user_name),
js(col),
js(lin),
js(value),
)
self.send_update(page, t)
else:
page.request += 1
line[a_column.data_col] = line[a_column.data_col].set_comment(value)
return 'ok.png'
def empty(self, empty_even_if_used_page=False,
empty_even_if_created_today=False,
empty_even_if_column_created=False,
):
if not empty_even_if_used_page and self.active_pages:
return False, utilities._("MSG_document_table_active")
if not empty_even_if_created_today and time.time()-self.mtime <24*3600:
return False, utilities._("MSG_document_table_today")
if self.comment:
return False, utilities._("MSG_document_table_comment"
) % cgi.escape(self.comment)
if not empty_even_if_column_created:
for c in self.columns:
if c.author != data.ro_user:
return False, utilities._("MSG_document_table_title")
for line in self.lines.values():
for j in line:
if not j.empty():
return False, utilities._("MSG_document_table_cell")
if j.comment:
return False, utilities._("MSG_document_table_cell_comment")
return True, utilities._("MSG_document_table_lines") % len(self.lines)
def problem_in_column_name(self):
names = set()
for c in self.columns:
if c.title in names:
return utilities._("MSG_document_table_duplicate") + c.title
names.add(c.title)
for c in self.columns:
for name in c.depends_on():
if name not in names:
return utilities._("MSG_document_table_no_column") % (
c.title, name)
return ""
def table_comment(self, page, comment):
"""Deprecated"""
return self.table_attr(page, 'comment', comment)
def add_empty_column(self, page, the_id):
if self.columns.columns:
position = max([c.position for c in self.columns]) + 1
else:
position = 0
a_column = column.Column(the_id, page.user_name, position=position)
self.columns.append(a_column)
for line in self.lines.values():
line.append(cellempty)
return a_column
def column_attr(self, page, col_id, attr, value):
col = self.columns.from_id(col_id)
if col is None:
col = self.add_empty_column(page, col_id)
return column.ColumnAttr.attrs[attr].set(self, page, col, value)
def column_changed(self, a_column, attribute):
"An column attribute change, column content may be updated"
column_changed_list.append((self, a_column, attribute))
def table_attr(self, page, attr, value):
return column.TableAttr.attrs[attr].set(self, page, value)
def column_comment(self, page, col_id, comment):
"""DEPRECATED : use column_attr"""
self.column_attr(page, col_id, 'comment', comment)
def column_change(self, page, col_id, title, ttype, test, weight,
freezed, hidden=0, width=1):
"""DEPRECATED : use column_attr"""
col = self.columns.from_id(col_id)
if col is None:
col = self.add_empty_column(page, col_id)
attrs = column.ColumnAttr.attrs
attrs['type'].set(self, page, col, ttype)
attrs['title'].set(self, page, col, title)
attrs['freezed'].set(self, page, col, freezed)
attrs['hidden'].set(self, page, col, hidden)
attrs['width'].set(self, page, col, width)
t = test.split(';')
if len(t) == 4:
test = '[' + t[0].strip('[') + ';' + t[3].strip(']') + ']'
attrs['red'].set(self, page, col, t[1])
attrs['green'].set(self, page, col, t[2])
if ttype == 'Nmbr':
attrs['test_filter'].set(self, page, col, test)
else:
if test:
attrs['minmax'].set(self, page, col, test)
t = re.split(' +', weight)
if len(t) > 1:
weight = t[0]
attrs['columns'].set(self, page, col, ' '.join(t[1:]))
if weight:
attrs['weight'].set(self, page, col, weight)
def column_delete(self, page, col):
if not self.loading and not self.modifiable:
return self.bad_ro(page)
a_column = self.columns.from_id(col)
if a_column == None:
return self.bad_column(page)
# The first 'if' is to allow teachers to destroy
# Columns created by 'data.ro_user' if they are not firsts ones.
# THIS SHOULD BE REMOVED, but not yet.
# It was tested because bad columns were created.
if page.user_name not in self.masters and a_column.data_col < 6 :
if not self.authorized_column(page.user_name, a_column):
return self.bad_auth(page, "column_delete %s" % col)
if (a_column.type.cell_is_modifiable
and not a_column.empty_of_user_values()):
return self.error(page, '_("ALERT_delete_not_empty")')
if not self.loading:
self.log('column_delete(%s,%s)' % (page.page_id, repr(col)))
t = '<script>Xcolumn_delete(%s,%s);</script>\n' % (
js(page.user_name), js(col))
self.send_update(page, t)
else:
page.request += 1
for line in self.lines.values():
line.pop(a_column.data_col)
self.columns.pop(a_column.data_col)
return 'ok.png'
def column_inscrit(self):
for i, c in enumerate(self.columns):
if c.freezed == 'C':
return i
return None
def content_head(self, page):
return table_head(
self.year, self.semester, page.ticket, page.user_name,
page.page_id, self.ue,
attrs_from=self, table=self
)
def date_change(self, page, date):
"""Deprecated"""
return column.TableAttr.attrs['dates'].set(self, page, date)
def logins(self):
for v in self.lines.values():
if v[0].value != '':
yield v[0].value
def logins_valid(self):
if self.column_inscrit() is None:
for v in self.lines.values():
if v[0].value != '':
yield v[0].value
else:
for v in self.lines.values():
if v[0].value != '' and v[5].value != 'non':
yield v[0].value
def the_keys(self):
"""Returns a dictionary for fast access by student login"""
return self.the_key_dict
def get_lines(self, login):
login = utilities.the_login(login)
if login in self.the_key_dict:
for lin in self.the_key_dict[login]:
yield self.lines[lin]
return
def lines_of_grp(self, grp, seq):
if grp == '':
return # Do not auto compute ABINJ??? if there is no real groups
grp_col = self.columns.get_grp()
seq_col = self.columns.get_seq()
if grp_col and seq_col:
for line in self.lines.values():
if line[grp_col].value == grp and line[seq_col].value == seq:
yield line
def get_items(self, login):
login = utilities.the_login(login)
if login in self.the_key_dict:
for lin in self.the_key_dict[login]:
yield lin, self.lines[lin]
return
def authors(self):
"""List of all the logins that have modified a cell value"""
a = {}
for v in self.lines.values():
for cell in v:
if cell.author not in ('', data.ro_user):
a[cell.author] = True
return list(a.keys())
def update_columns(self, columns, ro_page=None):
"""Update the default columns of the table.
This can be called by the TEMPLATE 'check' method
"""
if not self.modifiable:
return
if ro_page is None:
ro_page = self.pages[0]
locked = self.the_lock.locked()
try:
if not locked:
self.lock()
for col in sorted(columns):
for attr, value in columns[col].items():
value = str(value)
a_column = self.columns.from_id(col)
if a_column is None or value!= str(getattr(a_column, attr)):
self.column_attr(ro_page, col, attr, value)
finally:
if not locked:
self.unlock()
def content(self, page):
warn('%s content for page %d' % (
self.filename, page.page_id))
s = []
s.append(self.content_head(page))
try:
s.append(' <script><!--')
s.append(utilities.wait_scripts())
s.append('--></script> ')
lines = self.lines.js()
i = 0
nb = 100
to_load = len(lines)
while lines:
s.append('<script><!--')
s.append('\n'.join(lines[:nb]))
del lines[:nb]
i += nb
s.append('--></script><div class="loading_bar"><div style="width:%d%%"> </div></div>'
% int(i*100./to_load) )
s.append(' <style>DIV.loading_bar { display: none;}</style>')
s.append(''' <script><!--
function initialize()
{
if ( ! wait_scripts("initialize()") )
return ;
var t = document.getElementsByTagName("DIV");
for(var i=0; i<t.length; i++)
if ( t[i].className == "loading_bar" )
t[i].parentNode.removeChild(t[i]) ;
document.write(head_html()) ;
insert_middle();
''')
s.append(self.columns.js(hide=False))
s.append('document.write(tail_html());')
s.append('runlog(columns, lines) ;')
s.append('set_updating(%s) ;' % int(self in update_students))
s.append('}')
if self.new_abjs:
s.append(self.new_abjs)
if self.template and hasattr(self.template, 'content'):
s.append(self.template.content(self))
s.append('initialize();')
s.append('--></script> \n')
if self.template and hasattr(self.template, 'css'):
s.append('<style>' + self.template.css + '</style>')
finally:
pass
return '\n'.join(s)
def location(self):
return '%d %s %s' % (self.year, self.semester, self.ue)
def rewrite(self, only_columns=False, user_name=''):
s = ['# -*- coding: utf8 -*-']
s.append('from data import *')
s.append("new_page('' ,'" + data.ro_user + "', '', '')")
pages = collections.defaultdict(lambda: 1)
pages[data.ro_user] = 0
if user_name:
s.append("new_page('' ,'%s', '', '')" % user_name)
else:
authors = set()
for c in self.columns:
authors.add(c.author)
for line in self.lines.values():
for cell in line:
authors.add(cell.author)
authors.discard(data.ro_user)
for author in authors:
pages[author] = len(pages)
s.append("new_page('' ,'%s', '', '')" % author)
for c in self.columns:
# if c.empty() and c.type.cell_compute == 'undefined':
# continue
for attr in column.ColumnAttr.attrs.values():
if attr.name == 'author':
continue
attr_value = attr.decode(getattr(c, attr.name))
if attr_value != attr.default_value:
s.append('column_attr(%s,%d,%s,%s)' % (
repr(attr.name), pages[c.author],
repr(c.the_id), repr(attr_value)))
for attr in column.TableAttr.attrs.values():
if attr.computed:
continue
if attr.name in ('modifiable', 'dates'):
continue
attr_value = getattr(self, attr.name)
if attr.name == 'masters':
if user_name not in attr_value:
attr_value.append(user_name)
if '' in attr_value:
attr_value.remove('')
attr_value = attr.decode(attr_value)
if attr_value != attr.default_value:
s.append('table_attr(%s,0,%s)' % (
repr(attr.name),
repr(attr_value)))
if only_columns:
return '\n'.join(s) + '\n'
for line_key, line in self.lines.items():
for col, cell in zip(self.columns, line):
if cell.value != '':
s.append('cell_change(%d,%s,%s,%s,%s)' % (
pages[cell.author],
repr(col.the_id),
repr(line_key),
repr(cell.value),
repr(cell.date),
))
if cell.comment:
s.append('comment_change(%d,%s,%s,%s)' % (
pages[cell.author],
repr(col.the_id),
repr(line_key),
repr(cell.comment),
))
return '\n'.join(s) + '\n'
def close_active_pages(self):
for page in self.active_pages:
try:
page.browser_file.write('<script>window.parent.close();</script>')
page.browser_file.close()
except:
pass
self.active_pages = []
def do_not_unload_add(self, value):
if value:
self.do_not_unload.append(value)
def do_not_unload_remove(self, value):
self.do_not_unload.remove(value)
def unload(self, force=False):
if force:
self.close_active_pages()
self.do_not_unload = []
if '*' in ''.join(self.do_not_unload):
return
warn(str(self.ue), what="table")
# XXX
# In rare cases the table is unloaded while the student list
# is being updated. So we finish with a table half updated
# But it is not important.
deleted = tables_manage("del", self.year, self.semester, self.ue)
try:
if deleted:
update_students.remove(self) # No update student list.
except ValueError:
pass
return deleted
def delete(self):
warn(str(self.ue), what="table")
self.unload(force=True)
if not self.unloaded:
return
# XXX Not locked, so the table may be reloaded before deletion....
utilities.unlink_safe(self.filename)
utilities.unlink_safe(self.filename + 'c', do_backup=False)
for name in self.masters:
self.master_of_update('-', name)
for login in self.the_keys():
indexes_to_update.append((self, login, ''))
def send_alert(self, text):
self.send_update(None, '<script>alert(_("ALERT_message_for")+%s);</script>\n' %
js(self.ue + '\n\n' + text))
def __str__(self):
return '%d/%s/%s' % (self.year, self.semester, self.ue)
def backtrace_html(self):
return "Table: " + str(self)
def readable_by(self, the_ticket):
if (self.private
and the_ticket.user_name not in self.masters
and the_ticket.user_name not in configuration.root
):
warn('Unauthorized access', what='table')
return False
return True
def master_of_update(self, what, name):
if self.semester in configuration.master_of_exceptions:
return
d = utilities.manage_key('LOGINS', os.path.join(name, 'master_of'))
if d is False:
d = []
else:
d = eval(d)
t = (str(self.year), self.semester, self.ue)
if (t in d) and what == '-':
d.remove(t)
elif (t not in d) and what == '+':
d.append(t)
else:
return
utilities.manage_key('LOGINS', os.path.join(name, 'master_of'),
content = repr(d))
def the_abjs(self):
from . import abj
grp_col = self.columns.get_grp()
seq_col = self.columns.get_seq()
t = []
for login in self.logins():
tt = abj.tierstemps(login)
student = abj.Abj(self.year, self.semester, login)
line = list(self.get_lines(login))
if line:
line = line[0]
the_abjs= abj.do_prune(
student.abjs,
self.dates[0], self.dates[1]+86400,
grp_col and line[grp_col].value or '',
seq_col and line[seq_col].value or '',
self.ue)
else:
the_abjs = ()
da = student.da
if tt or the_abjs or da:
t.append("%s:[[%s],[%s],%s]" % (
js(login),
','.join(['[%s,%s,%s]' % (js(a),js(b),js(d))
for a,b,dummy_c,d in the_abjs]),
','.join(['[%s,%s,%s]' % (js(a),js(b),js(d))
for a,b,dummy_c,d in da]),
js(tt.encode('utf-8'))))
self.new_abjs = 'change_abjs({%s});\n' % ',\n'.join(t)
return self.new_abjs
def update_the_abjs(self):
old_abjs = self.new_abjs
if self.the_abjs() != old_abjs:
self.send_update(None,'<script>' + self.new_abjs + '</script>')
def link_to(self):
ln = os.readlink(self.filename)[:-3].split(os.path.sep)
ue = ln[-1]
if len(ln) == 3:
assert(ln[0] == '..')
assert(ln[1][0] == 'S')
year = self.year
semestre = ln[1][1:]
elif len(ln) == 5:
assert(ln[0] == '..')
assert(ln[1] == '..')
assert(ln[2][0] == 'Y')
assert(ln[3][0] == 'S')
year = int(ln[2][1:])
semestre = ln[3][1:]
else:
assert(len(ln) == 1)
return year, semestre, ue
def send_alert(text):
for atable in tables_values():
atable.send_alert(text)
@utilities.add_a_lock
def tables_manage(action, year, semester, ue, do_not_unload=0, new_table=None):
if action == 'get':
try:
t = tables[year, semester, ue]
if t and do_not_unload:
t.do_not_unload_add(do_not_unload)
return t
except KeyError:
tables[year, semester, ue] = None
return False
elif action == 'del':
try:
t = tables[year, semester, ue]
if not t:
return # Yet destroyed
if t.do_not_unload:
warn('Unload of do_not_unload: '
+ str(t.ue) + repr(t.do_not_unload), what="warning")
return False
if t.active_pages:
warn('Unload with active pages.', what="warning")
return False
# write access to the table will make an error.
t.unloaded = True
utilities.unload_module(t.module) # 2009-09-07 Add this
del tables[year, semester, ue]
return True
except KeyError:
return # Yet destroyed
elif action == 'replace':
old = tables.get((year, semester, ue), None)
if old:
# XXX The module is unloaded just after the import.
# Why is it necessary to do it again here?
utilities.unload_module(old.module)
tables[year, semester, ue] = new_table
return new_table
else:
raise ValueError("Unknown action '%s'" % action)
def tables_values():
# Use list because the tables may change while creating the list.
# Do not use a tuple because it must be sortable
# XXX Need a lock ?
return [t
for t in tuple(tables.values())
if t not in (None, False, True)
]
def table(year, semester, ue, page=None, ticket=None, ro=False, create=True,
do_not_unload=0):
"""With a ticket : return table and page
Without ticket : return only the table
"""
# utilities.warn('%s/%s/%s page=%s ticket=%s ro=%s create=%s' % (
# year, semester, ue, page, ticket, ro, create))
if configuration.read_only:
ro = True
year = int(year)
t = tables_manage('get', year, semester, ue, do_not_unload)
if t is None:
# Somebody else is creating the table
while t is None:
time.sleep(0.1)
t = tables_manage('get', year, semester, ue, do_not_unload)
if t is False:
# I must create the table
# Only one thread can be here at the same time.
try:
if ticket == None:
if (not create
and not os.path.exists(table_filename(year, semester, ue))):
del tables[year, semester, ue]
return None
t = Table(year, semester, ue, ro)
else:
t = Table(year, semester, ue, ro, user=ticket.user_name)
except:
# The table import failed, allow the new one to retry
# tables[year, semester, ue] = False
# raise
utilities.send_backtrace("", "Can't load %s/%s/%s" % (
year, semester, ue))
# tables[year, semester, ue] contains None
# So no need to call table_manage
del tables[year, semester, ue]
if ticket:
return None, None
else:
return None
t.do_not_unload_add(do_not_unload) # Only on this case
tables[year, semester, ue] = t
else:
# Table yet loaded
t = t.update()
t.atime = time.time()
if page is not None and not t.modifiable and page >= len(t.pages):
# Bug raised by TOMUSS restart (page list is lost)
# XXX : if 2 users are on the same unmodifiable table,
# then there is 50% chance that the second one is
# with the bad page and so the bad ticket.
# It will be accused of hacking.
page = None
if page == None:
if ticket == None:
return t
if not t.readable_by(ticket):
try:
t.do_not_unload_remove('new_page')
except ValueError:
pass
return None, None
# The new page may append work to check_students_in_tables
# And the work is done without call to 'table'.
# So possible simultaneous execution is possible.
t.lock()
try:
page = t.new_page(ticket.ticket,
ticket.user_name,
ticket.user_ip,
ticket.user_browser,
time.strftime('%Y%m%d%H%M%S'))
finally:
t.unlock()
return t, page
page = t.pages[page]
page.check_identity(ticket.ticket, ticket.user_name,
ticket.user_ip, ticket.user_browser)
return t, page
def remove_unused_tables():
"""Do not call from a thread"""
warn('start', what="table")
now = time.time()
for atable in tables_values():
if atable.active_pages:
continue
if now - atable.atime < configuration.unload_interval:
continue
atable.unload()
warn('stop', what="table")
# continuous update of students lists for all tables
def check_students_in_tables():
while True:
time.sleep(configuration.students_check_interval)
for t in tables_values():
update_students.append(t)
if configuration.regtest_sync:
time.sleep(0.1)
else:
time.sleep(10) # Avoid overload
# continuous update of students lists
update_students = []
@utilities.add_a_lock # The lock is here for regression tests
def check_new_students_real():
try:
while update_students:
t = update_students.pop()
t.do_not_unload_add('check_new_students_real')
# XXX Not a good test, but a lock is overkill
if t.unloaded:
continue
try:
t.send_update(None, "<script>set_updating(1);</script>")
utilities.bufferize_this_file(t.filename)
warn('start update students of %s' % t.ue, what="table")
if t.template and hasattr(t.template, 'check'):
t.template.check(t)
warn('done %s' % t.ue, what="table")
mails = inscrits.L_batch.mails(list(t.logins()) + t.authors())
mails.update(t.mails)
t.change_mails(mails)
if t.modifiable:
if t.force_update or getattr(t, 'update_inscrits', True):
for a_column in t.columns:
a_column.type.update_all(t, a_column)
t.force_update = 0
t.update_the_abjs()
finally:
t.do_not_unload_remove('check_new_students_real')
utilities.bufferize_this_file(None)
t.send_update(None, "<script>set_updating(0);</script>")
except IndexError:
if not configuration.regtest:
raise # Real problem
# XXX The regtest raise a bug, why ?
indexes_to_update = []
def update_index(login, action=None):
if not login:
return
if utilities.safe(login) != login:
return
c = utilities.manage_key('LOGINS', os.path.join(login, 'index'))
if c:
c = eval(c)
else:
c = []
if action is None:
return c
c = action(c) # Update index content
utilities.manage_key('LOGINS', os.path.join(login, 'index'),
content = repr(c))
def check_indexes_to_update():
while indexes_to_update:
tabl, old, new = indexes_to_update.pop(0)
def remove(x):
try:
x.remove((tabl.year, tabl.semester, tabl.ue))
except ValueError:
pass
return x
def append(x):
x.append((tabl.year, tabl.semester, tabl.ue))
return x
update_index(old, remove)
update_index(new, append)
def check_new_students():
while True:
if configuration.regtest_sync:
time.sleep(0.001)
else:
time.sleep(1)
check_indexes_to_update()
check_new_students_real()
def login_list(page, name):
# XXX Not very clean the : configuration.teachers[-1]
t = list(inscrits.L_slow.firstname_or_surname_to_logins(
name.replace('_',' '),
base=configuration.teachers[-1],
attributes = [configuration.attr_login,
configuration.attr_surname,
configuration.attr_firstname,
'cn']
))
t.sort(key = lambda x: x[1] + x[2])
if t:
s = []
for lo, surname, firstname, cn in t:
s.append('[' + utilities.js(lo.encode('utf-8'))
+ ',' + utilities.js(surname.upper().encode('utf-8'))
+ ',' + utilities.js(firstname.title().encode('utf-8'))
+ ',' + utilities.js(cn.encode('utf-8'))
+ ']')
else:
s = []
s = '<script>login_list(' + utilities.js(name) + \
',[' + ',\n'.join(s) + ']);</script>\n'
sender.append(page.browser_file, s)
# Do the real job to answer to modification requests
# It is complicated because we may receive request in bad order.
# So we keep a list of requests and sort it.
def it_is_a_bad_request(request, page, tabl, output_file):
if page.request > request:
# An old request was given. Assume same answer XXX
tabl.do_not_unload_remove('page_action')
try:
warn('Old request asked : %d in place of %d' % (
request, page.request))
output_file.write(files.files['ok.png'])
output_file.close()
except IOError:
pass
except:
utilities.send_backtrace('Exception request %d' % request)
return True
if tabl.unloaded:
# No sense to do the do_not_unload_add(-1)
utilities.send_backtrace('Request on unloaded table ' + tabl.ue,
exception=False)
return True
def should_be_delayed(request, page, tabl, r, t):
if tabl.the_lock.locked():
return True
if page.request < request:
# Count the number of identical request
n = 0
for old in t[::-1]:
if old[0] == r[0] and old[1] == r[1]:
n += 1
else:
break
if n > 12:
# We received 12 times the same request. (6 is too small)
# So, we hit a request-accounting bug.
# For example: server restart and browser 'update_content'
# request that is not stored in the table data file.
utilities.send_backtrace('Bad Request Number (%s!=%s)' %
(page.request, request), exception=False)
# We fix the page request number
# So we no more delay this request handling
page.request = request
sender.append(page.browser_file,
'<script>Alert("ERROR_server_bug");</script>')
else:
return True
def process_request(page, tabl, action, path):
page.request += 1
page.answer = 'bug.png'
tabl.lock()
try:
if action.startswith('column_attr_'):
page.answer = tabl.column_attr(page, path[0],
action[12:], path[1])
elif action.startswith('table_attr_'):
page.answer = tabl.table_attr(page,
action[11:], path[0])
elif action == 'cell_change':
col, lin, value = path
page.answer = tabl.cell_change(page, col, lin, value)
elif action == 'comment_change':
col, lin, value = path
page.answer = tabl.comment_change(page, col, lin, value)
elif action == 'column_delete':
col = path[0]
page.answer = tabl.column_delete(page, col)
elif action == 'login_list':
login_list(page, utilities.safe(path[0]))
page.answer = 'ok.png'
elif action == 'update_content':
if tabl not in update_students:
update_students.append(tabl)
tabl.force_update = 1
page.answer = 'ok.png'
else:
warn('BUG: %s' % str(path), what="error")
finally:
tabl.do_not_unload_remove('page_action')
tabl.unlock()
# We don't want asynchronous update when doing regtest
if configuration.regtest_sync:
update_students.append(tabl)
check_new_students_real()
request_list = []
def check_requests():
while True:
time.sleep(0.1)
my_request_list = list(request_list)
if len(my_request_list) == 0:
# To not flush buffers of currently student list updates
# In fact the buffering only gain 10% in user time and is
# a little less secure. So it is disabled (see YYY comments)
continue
my_request_list.sort()
valid_request = []
for r in my_request_list:
page_id, request, page, action, path, output_file = r
tabl = page.table
warn('R=%d P=%d A=%s P=%s DNU=%s' % (
request, page_id, action, path, tabl.do_not_unload),
what="DNU")
if it_is_a_bad_request(request, page, tabl, output_file):
request_list.remove(r)
continue
# utilities.bufferize_this_file(tabl.filename) # YYY
# It is only useful to compare to previous requests
valid_request.append(r)
if should_be_delayed(request, page, tabl, r, valid_request):
# do not decrement do_not_unload
# Keep in request_list
continue
# Remove from request list.
# If there is an error, browser will send a new request
request_list.remove(r)
try:
real_bug = True
process_request(page, tabl, action, path)
except:
warn('bug raised', what="error")
utilities.send_backtrace('check_requests: '+action+repr(path))
page.answer = 'bug.png'
real_bug = False
try:
warn('Send %s(%s) %s' % (output_file, output_file.closed,
page.answer), what="table")
if not output_file.closed:
output_file.write(files.files[page.answer])
output_file.close()
sender.append(page.browser_file,
'<script>saved(%d);</script>\n' % request,
index=len(tabl.sent_to_browsers)+1)
if page.answer == 'bug.png' and real_bug:
sender.append(page.browser_file,
'<script>Alert("ERROR_server_bug");</script>')
except socket.error:
pass
# utilities.bufferize_this_file(None) # YYY Flush buffers
# continuous send of packets to check connections
from . import ticket
def check_down_connections():
while True:
# This is done here, because testing this once per minute is fine.
# Force rewrite of modified files in TMP/version/file
# These files can be used by a static file server
for f in files.files.values():
try:
str(f)
except OSError:
pass
time.sleep(configuration.check_down_connections_interval)
for ttable in tables_values():
ttable.send_update(None, '<script>connected();</script>',
store=False)
for page in ttable.active_pages:
if page.ticket not in ticket.tickets:
warn('%s ticked expired page=%s ticket=%s' % (
ttable.filename,
page.page_id,
page.ticket), what='error')
continue
t = ticket.tickets[page.ticket]
if '-' in t.ticket:
sender.send_live_status('<script>d("%s","%s","%s",0.01,"pong", "%s",%s,%s);</script>\n'%(
t.user_ip,
t.ticket.split('-')[1] + '/' + t.user_name,
t.access_right(),
ttable.year, utilities.js(ttable.semester),
utilities.js(ttable.ue)))
# Update computed values
cell_changed_list_fast = []
def update_computed_values_fast():
while True:
time.sleep(0.1)
while cell_changed_list_fast:
the_table, lin, a_column = cell_changed_list_fast.pop()
for col in the_table.columns.use(a_column):
col.type.update_one(the_table, lin, col)
# Update mail if new teacher
login = the_table.lines[lin][a_column.data_col].author
if login not in the_table.mails:
m = inscrits.L_fast.mail(login)
if m:
the_table.update_mail(login, m.encode('utf-8'))
# Update mail if login changed
if a_column.data_col == 0:
login = the_table.lines[lin][0].value
if isinstance(login, str):
m = inscrits.L_fast.mail(login)
if m:
the_table.update_mail(login, m)
column_changed_list = []
def update_computed_values_slow():
while True:
time.sleep(0.1)
while column_changed_list:
the_table, a_column, attr = column_changed_list.pop()
a_column.type.update_all(the_table, a_column, attr)
for col in the_table.columns.use(a_column):
col.type.update_all(the_table, col)
def virtual_table(server, the_columns, the_lines, table_attrs={}, js=""):
"""Send the table to the browser without storage.
Do not use in a not threaded plugin.
"""
if not hasattr(server, "year"):
server.year = server.__dict__.get("the_year", 0)
if not hasattr(server, "semester"):
if hasattr(server, 'the_semester'):
server.semester = server.the_semester
else:
server.semester = "?"
for i, a_column in enumerate(the_columns):
if not a_column.position:
a_column.position = i
class TMP:
pass
tmp = TMP()
tmp.__dict__ = table_attrs
server.the_file.write(table_head(server.year,
server.semester,
server.ticket.ticket,
create_pref = False,
attrs_from=tmp,
user_name=server.ticket.user_name
)
)
lines = []
for i, line in enumerate(the_lines):
lines.append('"%d": %s' % (i, line.js()))
lines = '{' + ',\n'.join(lines) + '}'
columns = '[' + ',\n'.join([the_column.js(hide=False)
for the_column in the_columns]) + ']'
server.the_file.write("""
<script>
%s
%s
function initialize()
{
if ( ! wait_scripts("initialize()") )
return ;
document.write(head_html()) ;
insert_middle();
columns = %s ;
lines = %s ;
document.write(tail_html()) ;
table_attr.table_title = %s ;
runlog(columns, lines) ;
}
initialize() ;
</script>
""" % (utilities.wait_scripts(),
js, columns, lines, utilities.js(repr(server.the_path)) ))
logins = list(line[0].value
for line in the_lines
if line[0].value
)
mails = inscrits.L_batch.mails(logins)
server.the_file.write("<script>Xtable_attr('mails',%s);</script>\n"
% utilities.js(mails))
server.close_connection_now()
def start_threads():
utilities.start_new_thread_immortal(check_new_students, ())
utilities.start_new_thread_immortal(check_students_in_tables, ())
utilities.start_new_thread_immortal(check_requests, ())
utilities.start_new_thread_immortal(check_down_connections, ())
utilities.start_new_thread_immortal(update_computed_values_fast, ())
utilities.start_new_thread_immortal(update_computed_values_slow, ())