1
0
forked from ScoDoc/ScoDoc

Upload from subversion 1932

This commit is contained in:
viennet 2020-09-26 16:19:37 +02:00
parent 71c6f1f7a3
commit e9fee2c292
737 changed files with 204693 additions and 0 deletions

781
ImportScolars.py Normal file
View File

@ -0,0 +1,781 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2020 Emmanuel Viennet. All rights reserved.
#
# 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
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
""" Importation des etudiants à partir de fichiers CSV
"""
import os, sys, time, pdb
from sco_utils import *
from notesdb import *
from notes_log import log
import scolars
import sco_formsemestre
import sco_groups
import sco_excel
import sco_groups_view
import sco_news
from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
from sco_formsemestre_inscriptions import do_formsemestre_inscription_with_modules
from gen_tables import GenTable
# format description (relative to Product directory))
FORMAT_FILE = "misc/format_import_etudiants.txt"
# Champs modifiables via "Import données admission"
ADMISSION_MODIFIABLE_FIELDS = (
"code_nip",
"code_ine",
"date_naissance",
"lieu_naissance",
"bac",
"specialite",
"annee_bac",
"math",
"physique",
"anglais",
"francais",
"type_admission",
"boursier_prec",
"qualite",
"rapporteur",
"score",
"commentaire",
"classement",
"apb_groupe",
"apb_classement_gr",
"nomlycee",
"villelycee",
"codepostallycee",
"codelycee",
# Adresse:
"email",
"emailperso",
"domicile",
"codepostaldomicile",
"villedomicile",
"paysdomicile",
"telephone",
"telephonemobile",
# Debouche
"debouche",
# Groupes
"groupes",
)
# ----
def sco_import_format(with_codesemestre=True):
"returns tuples (Attribut, Type, Table, AllowNulls, Description)"
r = []
for l in open(SCO_SRCDIR + "/" + FORMAT_FILE):
l = l.strip()
if l and l[0] != "#":
fs = l.split(";")
if len(fs) < 5:
# Bug: invalid format file (fatal)
raise ScoException(
"file %s has invalid format (expected %d fields, got %d) (%s)"
% (FORMAT_FILE, 5, len(fs), l)
)
fieldname = (
fs[0].strip().lower().split()[0]
) # titre attribut: normalize, 1er mot seulement (nom du champ en BD)
typ, table, allow_nulls, description = [x.strip() for x in fs[1:5]]
aliases = [x.strip() for x in fs[5:] if x.strip()]
if fieldname not in aliases:
aliases.insert(0, fieldname) # prepend
if with_codesemestre or fs[0] != "codesemestre":
r.append((fieldname, typ, table, allow_nulls, description, aliases))
return r
def sco_import_format_dict(with_codesemestre=True):
""" Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }
"""
fmt = sco_import_format(with_codesemestre=with_codesemestre)
R = collections.OrderedDict()
for l in fmt:
R[l[0]] = {
"type": l[1],
"table": l[2],
"allow_nulls": l[3],
"description": l[4],
"aliases": l[5],
}
return R
def sco_import_generate_excel_sample(
fmt,
with_codesemestre=True,
only_tables=None,
with_groups=True,
exclude_cols=[],
extra_cols=[],
group_ids=[],
context=None,
REQUEST=None,
):
"""Generates an excel document based on format fmt
(format is the result of sco_import_format())
If not None, only_tables can specify a list of sql table names
(only columns from these tables will be generated)
If group_ids, liste les etudiants de ces groupes
"""
style = sco_excel.Excel_MakeStyle(bold=True)
style_required = sco_excel.Excel_MakeStyle(bold=True, color="red")
titles = []
titlesStyles = []
for l in fmt:
name = strlower(l[0])
if (not with_codesemestre) and name == "codesemestre":
continue # pas de colonne codesemestre
if only_tables is not None and strlower(l[2]) not in only_tables:
continue # table non demandée
if name in exclude_cols:
continue # colonne exclue
if int(l[3]):
titlesStyles.append(style)
else:
titlesStyles.append(style_required)
titles.append(name)
if with_groups and "groupes" not in titles:
titles.append("groupes")
titlesStyles.append(style)
titles += extra_cols
titlesStyles += [style] * len(extra_cols)
if group_ids and context:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
context, group_ids, REQUEST=REQUEST
)
members = groups_infos.members
log(
"sco_import_generate_excel_sample: group_ids=%s %d members"
% (group_ids, len(members))
)
titles = ["etudid"] + titles
titlesStyles = [style] + titlesStyles
# rempli table avec données actuelles
lines = []
for i in members:
etud = context.getEtudInfo(etudid=i["etudid"], filled=True)[0]
l = []
for field in titles:
if field == "groupes":
sco_groups.etud_add_group_infos(
context, etud, groups_infos.formsemestre, sep=";"
)
l.append(etud["partitionsgroupes"])
else:
key = strlower(field).split()[0]
l.append(etud.get(key, ""))
lines.append(l)
else:
lines = [[]] # empty content, titles only
return sco_excel.Excel_SimpleTable(
titles=titles, titlesStyles=titlesStyles, SheetName="Etudiants", lines=lines
)
def students_import_excel(
context,
csvfile,
REQUEST=None,
formsemestre_id=None,
check_homonyms=True,
require_ine=False,
):
"import students from Excel file"
diag = scolars_import_excel_file(
csvfile,
context.Notes,
REQUEST,
formsemestre_id=formsemestre_id,
check_homonyms=check_homonyms,
require_ine=require_ine,
exclude_cols=["photo_filename"],
)
if REQUEST:
if formsemestre_id:
dest = "formsemestre_status?formsemestre_id=%s" % formsemestre_id
else:
dest = REQUEST.URL1
H = [context.sco_header(REQUEST, page_title="Import etudiants")]
H.append("<ul>")
for d in diag:
H.append("<li>%s</li>" % d)
H.append("</ul>")
H.append("<p>Import terminé !</p>")
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
return "\n".join(H) + context.sco_footer(REQUEST)
def scolars_import_excel_file(
datafile,
context,
REQUEST,
formsemestre_id=None,
check_homonyms=True,
require_ine=False,
exclude_cols=[],
):
"""Importe etudiants depuis fichier Excel
et les inscrit dans le semestre indiqué (et à TOUS ses modules)
"""
log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id)
cnx = context.GetDBConnexion(autocommit=False)
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
annee_courante = time.localtime()[0]
always_require_ine = context.get_preference("always_require_ine")
exceldata = datafile.read()
if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide")
diag, data = sco_excel.Excel_to_list(exceldata)
if not data: # probably a bug
raise ScoException("scolars_import_excel_file: empty file !")
formsemestre_to_invalidate = Set()
# 1- --- check title line
titles = {}
fmt = sco_import_format()
for l in fmt:
tit = strlower(l[0]).split()[0] # titles in lowercase, and take 1st word
if (
(not formsemestre_id) or (tit != "codesemestre")
) and tit not in exclude_cols:
titles[tit] = l[1:] # title : (Type, Table, AllowNulls, Description)
# log("titles=%s" % titles)
# remove quotes, downcase and keep only 1st word
try:
fs = [strlower(stripquotes(s)).split()[0] for s in data[0]]
except:
raise ScoValueError("Titres de colonnes invalides (ou vides ?)")
# log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
# check columns titles
if len(fs) != len(titles):
missing = {}.fromkeys(titles.keys())
unknown = []
for f in fs:
if missing.has_key(f):
del missing[f]
else:
unknown.append(f)
raise ScoValueError(
"Nombre de colonnes incorrect (devrait être %d, et non %d) <br/> (colonnes manquantes: %s, colonnes invalides: %s)"
% (len(titles), len(fs), missing.keys(), unknown)
)
titleslist = []
for t in fs:
if not titles.has_key(t):
raise ScoValueError('Colonne invalide: "%s"' % t)
titleslist.append(t) #
# ok, same titles
# Start inserting data, abort whole transaction in case of error
created_etudids = []
NbImportedHomonyms = 0
GroupIdInferers = {}
try: # --- begin DB transaction
linenum = 0
for line in data[1:]:
linenum += 1
# Read fields, check and convert type
values = {}
fs = line
# remove quotes
for i in range(len(fs)):
if fs[i] and (
(fs[i][0] == '"' and fs[i][-1] == '"')
or (fs[i][0] == "'" and fs[i][-1] == "'")
):
fs[i] = fs[i][1:-1]
for i in range(len(fs)):
val = fs[i].strip()
typ, table, an, descr, aliases = tuple(titles[titleslist[i]])
# log('field %s: %s %s %s %s'%(titleslist[i], table, typ, an, descr))
if not val and not an:
raise ScoValueError(
"line %d: null value not allowed in column %s"
% (linenum, titleslist[i])
)
if val == "":
val = None
else:
if typ == "real":
val = val.replace(",", ".") # si virgule a la française
try:
val = float(val)
except:
raise ScoValueError(
"valeur nombre reel invalide (%s) sur line %d, colonne %s"
% (val, linenum, titleslist[i])
)
elif typ == "integer":
try:
# on doit accepter des valeurs comme "2006.0"
val = val.replace(",", ".") # si virgule a la française
val = float(val)
if val % 1.0 > 1e-4:
raise ValueError()
val = int(val)
except:
raise ScoValueError(
"valeur nombre entier invalide (%s) sur ligne %d, colonne %s"
% (val, linenum, titleslist[i])
)
# xxx Ad-hoc checks (should be in format description)
if strlower(titleslist[i]) == "sexe":
try:
val = scolars.normalize_sexe(val)
except:
raise ScoValueError(
"valeur invalide pour 'SEXE' (doit etre 'M' ou 'MME' ou 'H' ou 'F', pas '%s') ligne %d, colonne %s"
% (val, linenum, titleslist[i])
)
# Excel date conversion:
if strlower(titleslist[i]) == "date_naissance":
if val:
if re.match("^[0-9]*\.?[0-9]*$", str(val)):
val = sco_excel.xldate_as_datetime(float(val))
# INE
if (
strlower(titleslist[i]) == "code_ine"
and always_require_ine
and not val
):
raise ScoValueError(
"Code INE manquant sur ligne %d, colonne %s"
% (linenum, titleslist[i])
)
# --
values[titleslist[i]] = val
skip = False
is_new_ine = values["code_ine"] and _is_new_ine(cnx, values["code_ine"])
if require_ine and not is_new_ine:
log("skipping %s (code_ine=%s)" % (values["nom"], values["code_ine"]))
skip = True
if not skip:
if values["code_ine"] and not is_new_ine:
raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
# Check nom/prenom
ok, NbHomonyms = scolars.check_nom_prenom(
cnx, nom=values["nom"], prenom=values["prenom"]
)
if not ok:
raise ScoValueError(
"nom ou prénom invalide sur la ligne %d" % (linenum)
)
if NbHomonyms:
NbImportedHomonyms += 1
# Insert in DB tables
formsemestre_to_invalidate.add(
_import_one_student(
context,
cnx,
REQUEST,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
)
)
# Verification proportion d'homonymes: si > 10%, abandonne
log("scolars_import_excel_file: detected %d homonyms" % NbImportedHomonyms)
if check_homonyms and NbImportedHomonyms > len(created_etudids) / 10:
log("scolars_import_excel_file: too many homonyms")
raise ScoValueError(
"Il y a trop d'homonymes (%d étudiants)" % NbImportedHomonyms
)
except:
cnx.rollback()
log("scolars_import_excel_file: aborting transaction !")
# Nota: db transaction is sometimes partly commited...
# here we try to remove all created students
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
for etudid in created_etudids:
log("scolars_import_excel_file: deleting etudid=%s" % etudid)
cursor.execute(
"delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
{"etudid": etudid},
)
cursor.execute(
"delete from notes_formsemestre_inscription where etudid=%(etudid)s",
{"etudid": etudid},
)
cursor.execute(
"delete from scolar_events where etudid=%(etudid)s", {"etudid": etudid}
)
cursor.execute(
"delete from adresse where etudid=%(etudid)s", {"etudid": etudid}
)
cursor.execute(
"delete from admissions where etudid=%(etudid)s", {"etudid": etudid}
)
cursor.execute(
"delete from group_membership where etudid=%(etudid)s",
{"etudid": etudid},
)
cursor.execute(
"delete from identite where etudid=%(etudid)s", {"etudid": etudid}
)
cnx.commit()
log("scolars_import_excel_file: re-raising exception")
raise
diag.append("Import et inscription de %s étudiants" % len(created_etudids))
sco_news.add(
context,
REQUEST,
typ=NEWS_INSCR,
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
% len(created_etudids),
object=formsemestre_id,
)
log("scolars_import_excel_file: completing transaction")
cnx.commit()
# Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
context.Notes._inval_cache(formsemestre_id_list=formsemestre_to_invalidate)
return diag
def _import_one_student(
context,
cnx,
REQUEST,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
):
"""
Import d'un étudiant et inscription dans le semestre.
Return: id du semestre dans lequel il a été inscrit.
"""
log(
"scolars_import_excel_file: formsemestre_id=%s values=%s"
% (formsemestre_id, str(values))
)
# Identite
args = values.copy()
etudid = scolars.identite_create(cnx, args, context=context, REQUEST=REQUEST)
created_etudids.append(etudid)
# Admissions
args["etudid"] = etudid
args["annee"] = annee_courante
adm_id = scolars.admission_create(cnx, args)
# Adresse
args["typeadresse"] = "domicile"
args["description"] = "(infos admission)"
adresse_id = scolars.adresse_create(cnx, args)
# Inscription au semestre
args["etat"] = "I" # etat insc. semestre
if formsemestre_id:
args["formsemestre_id"] = formsemestre_id
else:
args["formsemestre_id"] = values["codesemestre"]
formsemestre_id = values["codesemestre"]
# recupere liste des groupes:
if formsemestre_id not in GroupIdInferers:
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(
context, formsemestre_id
)
gi = GroupIdInferers[formsemestre_id]
if args["groupes"]:
groupes = args["groupes"].split(";")
else:
groupes = []
group_ids = [gi[group_name] for group_name in groupes]
group_ids = {}.fromkeys(group_ids).keys() # uniq
if None in group_ids:
raise ScoValueError(
"groupe invalide sur la ligne %d (groupe %s)" % (linenum, groupes)
)
do_formsemestre_inscription_with_modules(
context,
args["formsemestre_id"],
etudid,
group_ids,
etat="I",
REQUEST=REQUEST,
method="import_csv_file",
)
return args["formsemestre_id"]
def _is_new_ine(cnx, code_ine):
"True if this code is not in DB"
etuds = scolars.identite_list(cnx, {"code_ine": code_ine})
return not etuds
# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
def scolars_import_admission(
datafile, context, REQUEST, formsemestre_id=None, type_admission=None
):
"""Importe données admission depuis un fichier Excel quelconque
par exemple ceux utilisés avec APB
Cherche dans ce fichier les étudiants qui correspondent à des inscrits du
semestre formsemestre_id.
Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait
via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux
étant ignorés).
On tolère plusieurs variantes pour chaque nom de colonne (ici aussi, la casse, les espaces
et les caractères spéciaux sont ignorés. Ainsi, la colonne "Prénom:" sera considéré comme "prenom".
Le parametre type_admission remplace les valeurs vides (dans la base ET dans le fichier importé) du champ type_admission.
Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré.
TODO:
- choix onglet du classeur
"""
log("scolars_import_admission: formsemestre_id=%s" % formsemestre_id)
members = sco_groups.get_group_members(
context, sco_groups.get_default_group(context, formsemestre_id)
)
etuds_by_nomprenom = {} # { nomprenom : etud }
diag = []
for m in members:
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
if np in etuds_by_nomprenom:
msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"])
log(msg)
diag.append(msg)
etuds_by_nomprenom[np] = m
exceldata = datafile.read()
diag2, data = sco_excel.Excel_to_list(exceldata, convert_to_string=False)
if not data:
raise ScoException("scolars_import_admission: empty file !")
diag += diag2
cnx = context.GetDBConnexion()
titles = data[0]
# idx -> ('field', convertor)
fields = adm_get_fields(titles, formsemestre_id)
idx_nom = None
idx_prenom = None
for idx in fields:
if fields[idx][0] == "nom":
idx_nom = idx
if fields[idx][0] == "prenom":
idx_prenom = idx
if (idx_nom is None) or (idx_prenom is None):
log("fields indices=" + ", ".join([str(x) for x in fields]))
log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
raise FormatError(
"scolars_import_admission: colonnes nom et prenom requises",
dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
% formsemestre_id,
)
modifiable_fields = Set(ADMISSION_MODIFIABLE_FIELDS)
nline = 2 # la premiere ligne de donnees du fichier excel est 2
n_import = 0
for line in data[1:]:
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
nom = adm_normalize_string(line[idx_nom])
prenom = adm_normalize_string(line[idx_prenom])
if not (nom, prenom) in etuds_by_nomprenom:
log(
"unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
)
else:
etud = etuds_by_nomprenom[(nom, prenom)]
cur_adm = scolars.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
# peuple les champs presents dans le tableau
args = {}
for idx in fields:
field_name, convertor = fields[idx]
if field_name in modifiable_fields:
try:
val = convertor(line[idx])
except ValueError:
raise FormatError(
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
% (nline, field_name, line[idx]),
dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
% formsemestre_id,
)
if val is not None: # note: ne peut jamais supprimer une valeur
args[field_name] = val
if args:
args["etudid"] = etud["etudid"]
args["adm_id"] = cur_adm["adm_id"]
# Type admission: traitement particulier
if not cur_adm["type_admission"] and not args.get("type_admission"):
args["type_admission"] = type_admission
scolars.etudident_edit(cnx, args)
adr = scolars.adresse_list(cnx, args={"etudid": etud["etudid"]})
if adr:
args["adresse_id"] = adr[0]["adresse_id"]
scolars.adresse_edit(
cnx, args
) # ne passe pas le contexte: pas de notification ici
else:
args["typeadresse"] = "domicile"
args["description"] = "(infos admission)"
adresse_id = scolars.adresse_create(cnx, args)
# log('import_adm: %s' % args )
# Change les groupes si nécessaire:
if args["groupes"]:
gi = sco_groups.GroupIdInferer(context, formsemestre_id)
groupes = args["groupes"].split(";")
group_ids = [gi[group_name] for group_name in groupes]
group_ids = {}.fromkeys(group_ids).keys() # uniq
if None in group_ids:
raise ScoValueError(
"groupe invalide sur la ligne %d (groupe %s)"
% (nline, groupes)
)
for group_id in group_ids:
sco_groups.change_etud_group_in_partition(
context, args["etudid"], group_id, REQUEST=REQUEST
)
#
diag.append("import de %s" % (etud["nomprenom"]))
n_import += 1
nline += 1
diag.append("%d lignes importées" % n_import)
if n_import > 0:
context._inval_cache(formsemestre_id=formsemestre_id)
return diag
_ADM_PATTERN = re.compile(r"[\W]+", re.UNICODE) # supprime tout sauf alphanum
def adm_normalize_string(s): # normalize unicode title
return suppression_diacritics(_ADM_PATTERN.sub("", s.strip().lower())).replace(
"_", ""
)
def adm_get_fields(titles, formsemestre_id):
"""Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
return: { idx : (field_name, convertor) }
"""
# log('adm_get_fields: titles=%s' % titles)
Fmt = sco_import_format_dict()
fields = {}
idx = 0
for title in titles:
title_n = adm_normalize_string(title)
for k in Fmt:
for v in Fmt[k]["aliases"]:
if adm_normalize_string(v) == title_n:
typ = Fmt[k]["type"]
if typ == "real":
convertor = adm_convert_real
elif typ == "integer" or typ == "int":
convertor = adm_convert_int
else:
convertor = adm_convert_text
# doublons ?
if k in [x[0] for x in fields.values()]:
raise FormatError(
'scolars_import_admission: titre "%s" en double (ligne 1)'
% (title),
dest_url="form_students_import_infos_admissions_apb?formsemestre_id=%s"
% formsemestre_id,
)
fields[idx] = (k, convertor)
idx += 1
return fields
def adm_convert_text(v):
if type(v) == FloatType:
return "{:g}".format(v) # evite "1.0"
return v
def adm_convert_int(v):
if type(v) != IntType and not v:
return None
return int(float(v)) # accept "10.0"
def adm_convert_real(v):
if type(v) != FloatType and not v:
return None
return float(v)
def adm_table_description_format(context):
"""Table HTML (ou autre format) decrivant les donnees d'admissions importables
"""
Fmt = sco_import_format_dict(with_codesemestre=False)
for k in Fmt:
Fmt[k]["attribute"] = k
Fmt[k]["aliases_str"] = ", ".join(Fmt[k]["aliases"])
if not Fmt[k]["allow_nulls"]:
Fmt[k]["required"] = "*"
if k in ADMISSION_MODIFIABLE_FIELDS:
Fmt[k]["writable"] = "oui"
else:
Fmt[k]["writable"] = "non"
titles = {
"attribute": "Attribut",
"type": "Type",
"required": "Requis",
"writable": "Modifiable",
"description": "Description",
"aliases_str": "Titres (variantes)",
}
columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
tab = GenTable(
titles=titles,
columns_ids=columns_ids,
rows=Fmt.values(),
html_sortable=True,
html_class="table_leftalign",
preferences=context.get_preferences(),
)
return tab

206
SuppressAccents.py Normal file
View File

@ -0,0 +1,206 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Suppression des accents d'une chaine
Source: http://wikipython.flibuste.net/moin.py/JouerAvecUnicode#head-1213938516c633958921591439c33d202244e2f4
"""
_reptable = {}
def _fill_reptable():
_corresp = [
(
u"A",
[0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104],
),
(u"AE", [0x00C6]),
(
u"a",
[0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105],
),
(u"ae", [0x00E6]),
(u"C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]),
(u"c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]),
(u"D", [0x00D0, 0x010E, 0x0110]),
(u"d", [0x00F0, 0x010F, 0x0111]),
(
u"E",
[0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A],
),
(
u"e",
[
0x00E8,
0xE9,
0x00E9,
0x00EA,
0xEB,
0x00EB,
0x0113,
0x0115,
0x0117,
0x0119,
0x011B,
],
),
(u"G", [0x011C, 0x011E, 0x0120, 0x0122]),
(u"g", [0x011D, 0x011F, 0x0121, 0x0123]),
(u"H", [0x0124, 0x0126]),
(u"h", [0x0125, 0x0127]),
(
u"I",
[0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130],
),
(
u"i",
[0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131],
),
(u"IJ", [0x0132]),
(u"ij", [0x0133]),
(u"J", [0x0134]),
(u"j", [0x0135]),
(u"K", [0x0136]),
(u"k", [0x0137, 0x0138]),
(u"L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]),
(u"l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]),
(u"N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]),
(u"n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]),
(
u"O",
[0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150],
),
(
u"o",
[0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151],
),
(u"OE", [0x0152]),
(u"oe", [0x0153]),
(u"R", [0x0154, 0x0156, 0x0158]),
(u"r", [0x0155, 0x0157, 0x0159]),
(u"S", [0x015A, 0x015C, 0x015E, 0x0160]),
(u"s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F, 0x0218]),
(u"T", [0x0162, 0x0164, 0x0166]),
(u"t", [0x0163, 0x0165, 0x0167]),
(
u"U",
[
0x00D9,
0x00DA,
0x00DB,
0x00DC,
0x0168,
0x016A,
0x016C,
0x016E,
0x0170,
0x172,
],
),
(
u"u",
[
0x00F9,
0x00FA,
0x00FB,
0x00FC,
0x0169,
0x016B,
0x016D,
0x016F,
0x0171,
0xB5,
],
),
(u"W", [0x0174]),
(u"w", [0x0175]),
(u"Y", [0x00DD, 0x0176, 0x0178]),
(u"y", [0x00FD, 0x00FF, 0x0177]),
(u"Z", [0x0179, 0x017B, 0x017D]),
(u"z", [0x017A, 0x017C, 0x017E]),
(
u"",
[
0x80,
0x81,
0x82,
0x83,
0x84,
0x85,
0x86,
0x87,
0x88,
0x89,
0x8A,
0x8B,
0x8C,
0x8D,
0x8E,
0x8F,
0x90,
0x91,
0x92,
0x93,
0x94,
0x95,
0x96,
0x97,
0x98,
0x99,
0x9A,
0x9B,
0x9C,
0x9D,
0x9E,
0x9F,
],
), # misc controls
(u" ", [0x00A0]), # &nbsp
(u"!", [0xA1]), # &iexcl;
(u"c", [0xA2]), # cent
(u"L", [0xA3]), # pound
(u"o", [0xA4]), # currency symbol
(u"Y", [0xA5]), # yen
(u"|", [0xA6]), # Broken Bar &brvbar;
(u"S", [0xA7]), # section
(u"", [0xA8]), # diaeresis ¨
(u"", [0xA9]), # copyright
(u'"', [0xAB, 0xBA]), # &laquo;, &raquo; <<, >>
(u" ", [0xAC]), # Math Not Sign
(u"", [0xAD]), # DashPunctuation
(u"(r)", [0xAE]), # registred
(u"-", [0xAF]), # macron
(u"", [0xB0]), # degre
(u"+-", [0xB1]), # +-
(u"2", [0x00B2, 0xB2]), # deux exposant
(u"3", [0xB3]), # 3 exposant
(u".", [0xB7]), # &middot;,
(u"1/4", [0xBC]), # 1/4
(u"1/2", [0xBD]), # 1/2
(u"3/4", [0xBE]), # 3/4
(u"e", [0x20AC]), # euro
(u"--", [0x2013]), # EN DASH
(u"'", [0x2018, 0x2019, 0x201A]), # LEFT, RIGHT SINGLE QUOTATION MARK
(u" ", [0x2020]), # dagger
]
global _reptable
for repchar, codes in _corresp:
for code in codes:
_reptable[code] = repchar
_fill_reptable()
def suppression_diacritics(s):
"""Suppression des accents et autres marques.
@param s: le texte à nettoyer.
@type s: str ou unicode
@return: le texte nettoyé de ses marques diacritiques.
@rtype: unicode
"""
if isinstance(s, str):
s = unicode(s, "utf8", "replace")
return s.translate(_reptable)

238
TODO Normal file
View File

@ -0,0 +1,238 @@
NOTES EN VRAC / Brouillon / Trucs obsoletes
#do_moduleimpl_list\(\{"([a-z_]*)"\s*:\s*(.*)\}\)
#do_moduleimpl_list( $1 = $2 )
#do_moduleimpl_list\([\s\n]*args[\s\n]*=[\s\n]*\{"([a-z_]*)"[\s\n]*:[\s\n]*(.*)[\s\n]*\}[\s\n]*\)
Upgrade JavaScript
- jquery-ui-1.12.1 introduit un problème d'affichage de la barre de menu.
Il faudrait la revoir entièrement pour upgrader.
On reste donc à jquery-ui-1.10.4.custom
Or cette version est incompatible avec jQuery 3 (messages d'erreur dans la console)
On reste donc avec jQuery 1.12.14
Suivi des requêtes utilisateurs:
table sql: id, ip, authuser, request
* Optim:
porcodeb4, avant memorisation des moy_ue:
S1 SEM14133 cold start: min 9s, max 12s, avg > 11s
inval (add note): 1.33s (pas de recalcul des autres)
inval (add abs) : min8s, max 12s (recalcule tout :-()
LP SEM14946 cold start: 0.7s - 0.86s
----------------- LISTE OBSOLETE (très ancienne, à trier) -----------------------
BUGS
----
- formsemestre_inscription_with_modules
si inscription 'un etud deja inscrit, IntegrityError
FEATURES REQUESTS
-----------------
* Bulletins:
. logos IUT et Univ sur bull PDF
. nom departement: nom abbrégé (CJ) ou complet (Carrière Juridiques)
. bulletin: deplacer la barre indicateur (cf OLDGEA S2: gêne)
. bulletin: click nom titre -> ficheEtud
. formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5"
et valider correctement le form !
* Jury
. recapcomplet: revenir avec qq lignes au dessus de l'étudiant en cours
* Divers
. formsemestre_editwithmodules: confirmer suppression modules
(et pour l'instant impossible si evaluations dans le module)
* Modules et UE optionnelles:
. UE capitalisées: donc dispense possible dans semestre redoublé.
traitable en n'inscrivant pas l'etudiant au modules
de cette UE: faire interface utilisateur
. page pour inscription d'un etudiant a un module
. page pour visualiser les modules auquel un etudiant est inscrit,
et le desinscrire si besoin.
. ficheEtud indiquer si inscrit au module sport
* Absences
. EtatAbsences : verifier dates (en JS)
. Listes absences pdf et listes groupes pdf + emargements (cf mail Nathalie)
. absences par demi-journées sur EtatAbsencesDate (? à vérifier)
. formChoixSemestreGroupe: utilisé par Absences/index_html
a améliorer
* Notes et évaluations:
. Exception "Not an OLE file": generer page erreur plus explicite
. Dates evaluation: utiliser JS pour calendrier
. Saisie des notes: si une note invalide, l'indiquer dans le listing (JS ?)
. et/ou: notes invalides: afficher les noms des etudiants concernes
dans le message d'erreur.
. upload excel: message erreur peu explicite:
* Feuille "Saisie notes", 17 lignes
* Erreur: la feuille contient 1 notes invalides
* Notes invalides pour les id: ['10500494']
(pas de notes modifiées)
Notes chargées. <<< CONTRADICTOIRE !!
. recap complet semestre:
Options:
- choix groupes
- critère de tri (moy ou alphab)
- nb de chiffres a afficher
+ definir des "catégories" d'évaluations (eg "théorie","pratique")
afin de n'afficher que des moyennes "de catégorie" dans
le bulletin.
. liste des absents à une eval et croisement avec BD absences
. notes_evaluation_listenotes
- afficher groupes, moyenne, #inscrits, #absents, #manquantes dans l'en-tete.
- lien vers modif notes (selon role)
. Export excel des notes d'evaluation: indiquer date, et autres infos en haut.
. Génération PDF listes notes
. Page recap notes moyennes par groupes (choisir type de groupe?)
. (GEA) edition tableau notes avec tous les evals d'un module
(comme notes_evaluation_listenotes mais avec tt les evals)
* Non prioritaire:
. optimiser scolar_news_summary
. recapitulatif des "nouvelles"
- dernieres notes
- changement de statuts (demissions,inscriptions)
- annotations
- entreprises
. notes_table: pouvoir changer decision sans invalider tout le cache
. navigation: utiliser Session pour montrer historique pages vues ?
------------------------------------------------------------------------
A faire:
- fiche etud: code dec jury sur ligne 1
si ancien, indiquer autorisation inscription sous le parcours
- saisie notes: undo
- saisie notes: validation
- ticket #18:
UE capitalisées: donc dispense possible dans semestre redoublé. Traitable en n'inscrivant pas l'etudiant aux modules de cette UE: faire interface utilisateur.
Prévoir d'entrer une UE capitalisée avec sa note, date d'obtention et un commentaire. Coupler avec la désincription aux modules (si l'étudiant a été inscrit avec ses condisciples).
- Ticket #4: Afin d'éviter les doublons, vérifier qu'il n'existe pas d'homonyme proche lors de la création manuelle d'un étudiant. (confirmé en ScoDoc 6, vérifier aussi les imports Excel)
- Ticket #74: Il est possible d'inscrire un étudiant sans prénom par un import excel !!!
- Ticket #64: saisir les absences pour la promo entiere (et pas par groupe). Des fois, je fais signer une feuille de presence en amphi a partir de la liste de tous les etudiants. Ensuite pour reporter les absents par groupe, c'est galere.
- Ticket #62: Lors des exports Excel, le format des cellules n'est pas reconnu comme numérique sous Windows (pas de problèmes avec Macintosh et Linux).
A confirmer et corriger.
- Ticket #75: On peut modifier une décision de jury (et les autorisations de passage associées), mais pas la supprimer purement et simplement.
Ajoute ce choix dans les "décisions manuelles".
- Ticket #37: Page recap notes moyennes par groupes
Construire une page avec les moyennes dans chaque UE ou module par groupe d'étudiants.
Et aussi pourquoi pas ventiler par type de bac, sexe, parcours (nombre de semestre de parcours) ?
redemandé par CJ: à faire avant mai 2008 !
- Ticket #75: Synchro Apogée: choisir les etudiants
Sur la page de syncho Apogée (formsemestre_synchro_etuds), on peut choisir (cocher) les étudiants Apogée à importer. mais on ne peut pas le faire s'ils sont déjà dans ScoDoc: il faudrait ajouter des checkboxes dans toutes les listes.
- Ticket #9: Format des valeurs de marges des bulletins.
formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5" et valider correctement le form !
- Ticket #17: Suppression modules dans semestres
formsemestre_editwithmodules: confirmer suppression modules
- Ticket #29: changer le stoquage des photos, garder une version HD.
- bencher NotesTable sans calcul de moyennes. Etudier un cache des moyennes de modules.
- listes d'utilisateurs (modules): remplacer menus par champs texte + completions javascript
- documenter archives sur Wiki
- verifier paquet Debian pour font pdf (reportab: helvetica ... plante si font indisponible)
- chercher comment obtenir une page d'erreur correcte pour les pages POST
(eg: si le font n'existe pas, archive semestre echoue sans page d'erreur)
? je ne crois pas que le POST soit en cause. HTTP status=500
ne se produit pas avec Safari
- essayer avec IE / Win98
- faire apparaitre les diplômés sur le graphe des parcours
- démission: formulaire: vérifier que la date est bien dans le semestre
+ graphe parcours: aligner en colonnes selon les dates (de fin), placer les diplomes
dans la même colone que le semestre terminal.
- modif gestion utilisateurs (donner droits en fct du dept. d'appartenance, bug #57)
- modif form def. utilisateur (dept appartenance)
- utilisateurs: source externe
- archivage des semestres
o-------------------------------------o
* Nouvelle gestion utilisateurs:
objectif: dissocier l'authentification de la notion "d'enseignant"
On a une source externe "d'utilisateurs" (annuaire LDAP ou base SQL)
qui permet seulement de:
- authentifier un utilisateur (login, passwd)
- lister un utilisateur: login => firstname, lastname, email
- lister les utilisateurs
et une base interne ScoDoc "d'acteurs" (enseignants, administratifs).
Chaque acteur est défini par:
- actor_id, firstname, lastname
date_creation, date_expiration,
roles, departement,
email (+flag indiquant s'il faut utiliser ce mail ou celui de
l'utilisateur ?)
state (on, off) (pour desactiver avant expiration ?)
user_id (login) => lien avec base utilisateur
On offrira une source d'utilisateurs SQL (base partagée par tous les dept.
d'une instance ScoDoc), mais dans la plupart des cas les gens utiliseront
un annuaire LDAP.
La base d'acteurs remplace ScoUsers. Les objets ScoDoc (semestres,
modules etc) font référence à des acteurs (eg responsable_id est un actor_id).
Le lien entre les deux ?
Loger un utilisateur => authentification utilisateur + association d'un acteur
Cela doit se faire au niveau d'un UserFolder Zope, pour avoir les
bons rôles et le contrôle d'accès adéquat.
(Il faut donc coder notre propre UserFolder).
On ne peut associer qu'un acteur à l'état 'on' et non expiré.
Opérations ScoDoc:
- paramétrage: choisir et paramétrer source utilisateurs
- ajouter utilisateur: choisir un utilisateur dans la liste
et lui associer un nouvel acteur (choix des rôles, des dates)
+ éventuellement: synchro d'un ensemble d'utilisateurs, basé sur
une requête (eg LDAP) précise (quelle interface utilisateur proposer ?)
- régulièrement (cron) aviser quelqu'un (le chef) de l'expiration des acteurs.
- changer etat d'un acteur (on/off)
o-------------------------------------o

769
TrivialFormulator.py Normal file
View File

@ -0,0 +1,769 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Simple form generator/validator
E. Viennet 2005 - 2008
v 1.2
"""
from types import *
def TrivialFormulator(
form_url,
values,
formdescription=(),
initvalues={},
method="post",
enctype=None,
submitlabel="OK",
name=None,
formid="tf",
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=[],
top_buttons=False, # place buttons at top of form
bottom_buttons=True, # buttons after form
html_foot_markup="",
readonly=False,
is_submitted=False,
):
"""
form_url : URL for this form
initvalues : dict giving default values
values : dict with all HTML form variables (may start empty)
is_submitted: handle form as if already submitted
Returns (status, HTML form, values)
status = 0 (html to display),
1 (ok, validated values in "values")
-1 cancel (if cancelbutton specified)
HTML form: html string (form to insert in your web page)
values: None or, when the form is submitted and correctly filled,
a dictionnary with the requeted values.
formdescription: sequence [ (field, description), ... ]
where description is a dict with following (optional) keys:
default : default value for this field ('')
title : text titre (default to field name)
allow_null : if true, field can be left empty (default true)
type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
readonly : default False. if True, no form element, display current value.
convert_numbers: covert int and float values (from string)
allowed_values : list of possible values (default: any value)
validator : function validating the field (called with (value,field)).
min_value : minimum value (for floats and ints)
max_value : maximum value (for floats and ints)
explanation: text string to display next the input widget
title_buble: help bubble on field title (needs bubble.js or equivalent)
comment : comment, showed under input widget
withcheckbox: if true, place a checkbox at the left of the input
elem. Checked items will be returned in 'tf-checked'
attributes: a liste of strings to put in the HTML form element
template: HTML template for element
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
'text_suggest'
(default text)
size : text field width
rows, cols: textarea geometry
labels : labels for radio or menu lists (associated to allowed_values)
vertical: for checkbox; if true, vertical layout
disabled_items: for checkbox, dict such that disabled_items[i] true if disabled checkbox
To use text_suggest elements, one must:
- specify options in text_suggest_options (a dict)
- HTML page must load JS AutoSuggest.js and CSS autosuggest_inquisitor.css
- bodyOnLoad must call JS function init_tf_form(formid)
"""
method = method.lower()
if method == "get":
enctype = None
t = TF(
form_url,
values,
formdescription,
initvalues,
method,
enctype,
submitlabel,
name,
formid,
cssclass,
cancelbutton=cancelbutton,
submitbutton=submitbutton,
submitbuttonattributes=submitbuttonattributes,
top_buttons=top_buttons,
bottom_buttons=bottom_buttons,
html_foot_markup=html_foot_markup,
readonly=readonly,
is_submitted=is_submitted,
)
form = t.getform()
if t.canceled():
res = -1
elif t.submitted() and t.result:
res = 1
else:
res = 0
return res, form, t.result
class TF:
def __init__(
self,
form_url,
values,
formdescription=[],
initvalues={},
method="POST",
enctype=None,
submitlabel="OK",
name=None,
formid="tf",
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=[],
top_buttons=False, # place buttons at top of form
bottom_buttons=True, # buttons after form
html_foot_markup="", # html snippet put at the end, just after the table
readonly=False,
is_submitted=False,
):
self.form_url = form_url
self.values = values
self.formdescription = list(formdescription)
self.initvalues = initvalues
self.method = method
self.enctype = enctype
self.submitlabel = submitlabel
if name:
self.name = name
else:
self.name = formid # 'tf'
self.formid = formid
self.cssclass = cssclass
self.cancelbutton = cancelbutton
self.submitbutton = submitbutton
self.submitbuttonattributes = submitbuttonattributes
self.top_buttons = top_buttons
self.bottom_buttons = bottom_buttons
self.html_foot_markup = html_foot_markup
self.readonly = readonly
self.result = None
self.is_submitted = is_submitted
if readonly:
self.top_buttons = self.bottom_buttons = False
self.cssclass += " readonly"
def submitted(self):
"true if form has been submitted"
if self.is_submitted:
return True
return self.values.get("%s-submitted" % self.formid, False)
def canceled(self):
"true if form has been canceled"
return self.values.get("%s_cancel" % self.formid, False)
def getform(self):
"return HTML form"
R = []
msg = None
self.setdefaultvalues()
if self.submitted() and not self.readonly:
msg = self.checkvalues()
# display error message
R.append(tf_error_message(msg))
# form or view
if self.readonly:
R = R + self._ReadOnlyVersion(self.formdescription)
else:
R = R + self._GenForm()
#
return "\n".join(R)
__str__ = getform
__repr__ = getform
def setdefaultvalues(self):
"set default values and convert numbers to strings"
for (field, descr) in self.formdescription:
# special case for boolcheckbox
if descr.get("input_type", None) == "boolcheckbox" and self.submitted():
if not self.values.has_key(field):
self.values[field] = 0
else:
self.values[field] = 1
if not self.values.has_key(field):
if descr.has_key("default"): # first: default in form description
self.values[field] = descr["default"]
else: # then: use initvalues dict
self.values[field] = self.initvalues.get(field, "")
if self.values[field] == None:
self.values[field] = ""
# convert numbers
if type(self.values[field]) == type(1) or type(self.values[field]) == type(
1.0
):
self.values[field] = str(self.values[field])
#
if not self.values.has_key("tf-checked"):
if self.submitted():
# si rien n'est coché, tf-checked n'existe plus dans la reponse
self.values["tf-checked"] = []
else:
self.values["tf-checked"] = self.initvalues.get("tf-checked", [])
self.values["tf-checked"] = [str(x) for x in self.values["tf-checked"]]
def checkvalues(self):
"check values. Store .result and returns msg"
ok = 1
msg = []
for (field, descr) in self.formdescription:
val = self.values[field]
# do not check "unckecked" items
if descr.get("withcheckbox", False):
if not field in self.values["tf-checked"]:
continue
# null values
allow_null = descr.get("allow_null", True)
if not allow_null:
if val == "" or val == None:
msg.append(
"Le champ '%s' doit être renseigné" % descr.get("title", field)
)
ok = 0
# type
typ = descr.get("type", "string")
if val != "" and val != None:
# check only non-null values
if typ[:3] == "int":
try:
val = int(val)
self.values[field] = val
except:
msg.append(
"La valeur du champ '%s' doit être un nombre entier" % field
)
ok = 0
elif typ == "float" or typ == "real":
self.values[field] = self.values[field].replace(",", ".")
try:
val = float(val.replace(",", ".")) # allow ,
self.values[field] = val
except:
msg.append(
"La valeur du champ '%s' doit être un nombre" % field
)
ok = 0
if typ[:3] == "int" or typ == "float" or typ == "real":
if descr.has_key("min_value") and val < descr["min_value"]:
msg.append(
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
% (val, field, descr["min_value"])
)
ok = 0
if descr.has_key("max_value") and val > descr["max_value"]:
msg.append(
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
% (val, field, descr["max_value"])
)
ok = 0
# allowed values
if descr.has_key("allowed_values"):
if descr.get("input_type", None) == "checkbox":
# for checkboxes, val is a list
for v in val:
if not v in descr["allowed_values"]:
msg.append(
"valeur invalide (%s) pour le champ '%s'" % (val, field)
)
ok = 0
elif descr.get("input_type", None) == "boolcheckbox":
pass
elif not val in descr["allowed_values"]:
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
if descr.has_key("validator"):
if not descr["validator"](val, field):
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
# boolean checkbox
if descr.get("input_type", None) == "boolcheckbox":
if int(val):
self.values[field] = 1
else:
self.values[field] = 0
# open('/tmp/toto','a').write('checkvalues: val=%s (%s) values[%s] = %s\n' % (val, type(val), field, self.values[field]))
if descr.get("convert_numbers", False):
if typ[:3] == "int":
self.values[field] = int(self.values[field])
elif typ == "float" or typ == "real":
self.values[field] = float(self.values[field].replace(",", "."))
if ok:
self.result = self.values
else:
self.result = None
return msg
def _GenForm(self, method="", enctype=None, form_url=""):
values = self.values
add_no_enter_js = False # add JS function to prevent 'enter' -> submit
# form template
# default template for each input element
itemtemplate = """<tr%(item_dom_attr)s>
<td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td>
</tr>
"""
hiddenitemtemplate = "%(elem)s"
separatortemplate = '<tr%(item_dom_attr)s><td colspan="2">%(label)s</td></tr>'
# ---- build form
buttons_markup = ""
if self.submitbutton:
buttons_markup += (
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s/>'
% (
self.formid,
self.formid,
self.submitlabel,
" ".join(self.submitbuttonattributes),
)
)
if self.cancelbutton:
buttons_markup += (
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
% (self.formid, self.formid, self.cancelbutton)
)
R = []
suggest_js = []
if self.enctype is None:
if self.method == "post":
enctype = "multipart/form-data"
else:
enctype = "application/x-www-form-urlencoded"
if self.cssclass:
klass = ' class="%s"' % self.cssclass
else:
klass = ""
name = self.name
R.append(
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
% (self.form_url, self.method, self.formid, enctype, name, klass)
)
R.append('<input type="hidden" name="%s-submitted" value="1"/>' % self.formid)
if self.top_buttons:
R.append(buttons_markup + "<p></p>")
R.append('<table class="tf">')
idx = 0
for idx in range(len(self.formdescription)):
(field, descr) = self.formdescription[idx]
nextitemname = None
if idx < len(self.formdescription) - 2:
nextitemname = self.formdescription[idx + 1][0]
if descr.get("readonly", False):
R.append(self._ReadOnlyElement(field, descr))
continue
wid = self.name + "_" + field
size = descr.get("size", 12)
rows = descr.get("rows", 5)
cols = descr.get("cols", 60)
title = descr.get("title", field.capitalize())
title_bubble = descr.get("title_bubble", None)
withcheckbox = descr.get("withcheckbox", False)
input_type = descr.get("input_type", "text")
item_dom_id = descr.get("dom_id", "")
if item_dom_id:
item_dom_attr = ' id="%s"' % item_dom_id
else:
item_dom_attr = ""
# choix du template
etempl = descr.get("template", None)
if etempl is None:
if input_type == "hidden":
etempl = hiddenitemtemplate
elif input_type == "separator":
etempl = separatortemplate
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
continue
else:
etempl = itemtemplate
lab = []
lem = []
if withcheckbox and input_type != "hidden":
if field in values["tf-checked"]:
checked = 'checked="checked"'
else:
checked = ""
lab.append(
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s/>'
% ("tf-checked", field, checked)
)
if title_bubble:
lab.append(
'<a class="discretelink" href="" title="%s">%s</a>'
% (title_bubble, title)
)
else:
lab.append(title)
#
attribs = " ".join(descr.get("attributes", []))
if (
withcheckbox and not checked
) or not descr.get( # desactive les element non coches:
"enabled", True
):
attribs += ' disabled="true"'
#
if input_type == "text":
lem.append(
'<input type="text" name="%s" size="%d" id="%s" %s'
% (field, size, wid, attribs)
)
if descr.get("return_focus_next", False): # and nextitemname:
# JS code to focus on next element on 'enter' key
# ceci ne marche que pour desactiver enter sous IE (pas Firefox)
# lem.append('''onKeyDown="if(event.keyCode==13){
# event.cancelBubble = true; event.returnValue = false;}"''')
lem.append('onkeypress="return enter_focus_next(this, event);"')
add_no_enter_js = True
# lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
# lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
lem.append(('value="%(' + field + ')s" />') % values)
elif input_type == "password":
lem.append(
'<input type="password" name="%s" id="%s" size="%d" %s'
% (field, wid, size, attribs)
)
lem.append(('value="%(' + field + ')s" />') % values)
elif input_type == "radio":
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
if descr["allowed_values"][i] == values[field]:
checked = 'checked="checked"'
else:
checked = ""
lem.append(
'<input type="radio" name="%s" value="%s" %s %s>%s</input>'
% (
field,
descr["allowed_values"][i],
checked,
attribs,
labels[i],
)
)
elif input_type == "menu":
lem.append('<select name="%s" id="%s" %s>' % (field, wid, attribs))
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
if str(descr["allowed_values"][i]) == str(values[field]):
selected = "selected"
else:
selected = ""
lem.append(
'<option value="%s" %s>%s</option>'
% (descr["allowed_values"][i], selected, labels[i])
)
lem.append("</select>")
elif input_type == "checkbox" or input_type == "boolcheckbox":
if input_type == "checkbox":
labels = descr.get("labels", descr["allowed_values"])
else: # boolcheckbox
labels = [""]
descr["allowed_values"] = ["0", "1"]
vertical = descr.get("vertical", False)
disabled_items = descr.get("disabled_items", {})
if vertical:
lem.append("<table>")
for i in range(len(labels)):
if input_type == "checkbox":
# from notes_log import log # debug only
# log('checkbox: values[%s] = "%s"' % (field,repr(values[field]) ))
# log("descr['allowed_values'][%s] = '%s'" % (i, repr(descr['allowed_values'][i])))
if descr["allowed_values"][i] in values[field]:
checked = 'checked="checked"'
else:
checked = ""
else: # boolcheckbox
# open('/tmp/toto','a').write('GenForm: values[%s] = %s (%s)\n' % (field, values[field], type(values[field])))
try:
v = int(values[field])
except:
v = 0
if v:
checked = 'checked="checked"'
else:
checked = ""
if vertical:
lem.append("<tr><td>")
if disabled_items.get(i, False):
disab = 'disabled="1"'
ilab = (
'<span class="tf-label-disabled">'
+ labels[i]
+ "</span> <em>(non modifiable)</em>"
)
else:
disab = ""
ilab = "<span>" + labels[i] + "</span>"
lem.append(
'<input type="checkbox" name="%s:list" value="%s" %s %s %s>%s</input>'
% (
field,
descr["allowed_values"][i],
attribs,
disab,
checked,
ilab,
)
)
if vertical:
lem.append("</tr></td>")
if vertical:
lem.append("</table>")
elif input_type == "textarea":
lem.append(
'<textarea name="%s" id="%s" rows="%d" cols="%d" %s>%s</textarea>'
% (field, wid, rows, cols, attribs, values[field])
)
elif input_type == "hidden":
if descr.get("type", "") == "list":
for v in values[field]:
lem.append(
'<input type="hidden" name="%s:list" value="%s" %s />'
% (field, v, attribs)
)
else:
lem.append(
'<input type="hidden" name="%s" id="%s" value="%s" %s />'
% (field, wid, values[field], attribs)
)
elif input_type == "separator":
pass
elif input_type == "file":
lem.append(
'<input type="file" name="%s" size="%s" value="%s" %s/>'
% (field, size, values[field], attribs)
)
elif input_type == "date": # JavaScript widget for date input
lem.append(
'<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
% (field, values[field])
)
elif input_type == "text_suggest":
lem.append(
'<input type="text" name="%s" id="%s" size="%d" %s'
% (field, field, size, attribs)
)
lem.append(('value="%(' + field + ')s" />') % values)
suggest_js.append(
"""var %s_opts = %s;
var %s_as = new bsn.AutoSuggest('%s', %s_opts);
"""
% (
field,
dict2js(descr.get("text_suggest_options", {})),
field,
field,
field,
)
)
else:
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
if explanation:
lem.append('<span class="tf-explanation">%s</span>' % explanation)
comment = descr.get("comment", "")
if comment:
lem.append('<br/><span class="tf-comment">%s</span>' % comment)
R.append(
etempl
% {
"label": "\n".join(lab),
"elem": "\n".join(lem),
"item_dom_attr": item_dom_attr,
}
)
R.append("</table>")
R.append(self.html_foot_markup)
if self.bottom_buttons:
R.append("<br/>" + buttons_markup)
if add_no_enter_js:
R.append(
"""<script type="text/javascript">
function enter_focus_next (elem, event) {
var cod = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
var enter = false;
if (event.keyCode == 13)
enter = true;
if (event.which == 13)
enter = true;
if (event.charCode == 13)
enter = true;
if (enter) {
var focused = false;
var i;
for (i = 0; i < elem.form.elements.length; i++)
if (elem == elem.form.elements[i])
break;
i = i + 1;
while (i < elem.form.elements.length) {
if ((elem.form.elements[i].type == "text")
&& (!(elem.form.elements[i].disabled))
&& ($(elem.form.elements[i]).is(':visible')))
{
elem.form.elements[i].focus();
focused = true;
break;
}
i = i + 1;
}
if (!focused) {
elem.blur();
}
return false;
}
else
return true;
}</script>
"""
) # enter_focus_next, ne focus que les champs text
if suggest_js:
# nota: formid is currently ignored
# => only one form with text_suggest field on a page.
R.append(
"""<script type="text/javascript">
function init_tf_form(formid) {
%s
}
</script>"""
% "\n".join(suggest_js)
)
# Javascript common to all forms:
R.append(
"""<script type="text/javascript">
// controle par la checkbox
function tf_enable_elem(checkbox) {
var oid = checkbox.value;
if (oid) {
var elem = document.getElementById(oid);
if (elem) {
if (checkbox.checked) {
elem.disabled = false;
} else {
elem.disabled = true;
}
}
}
}
// Selections etendues avec shift (use jquery.field)
$('input[name="tf-checked:list"]').createCheckboxRange();
</script>"""
)
R.append("</form>")
return R
def _ReadOnlyElement(self, field, descr):
"Generate HTML for an element, read-only"
R = []
title = descr.get("title", field.capitalize())
withcheckbox = descr.get("withcheckbox", False)
input_type = descr.get("input_type", "text")
klass = descr.get("cssclass", "")
klass = " " + klass
if input_type == "hidden":
return ""
R.append('<tr class="tf-ro-tr%s">' % klass)
if input_type == "separator": # separator
R.append('<td colspan="2">%s' % title)
else:
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
R.append("%s</td>" % title)
R.append('<td class="tf-ro-field%s">' % klass)
if input_type == "text" or input_type == "text_suggest":
R.append(("%(" + field + ")s") % self.values)
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
if input_type == "boolcheckbox":
labels = descr.get(
"labels", descr.get("allowed_values", ["oui", "non"])
)
# XXX open('/tmp/log', 'w').write('%s labels=%s, val=%s\ndescr=%s\n'%(field, labels, self.values[field], descr))
R.append(labels[int(self.values[field])])
if int(self.values[field]):
R.append('<input type="hidden" name="%s" value="1"/>' % field)
else:
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
if str(descr["allowed_values"][i]) == str(self.values[field]):
R.append('<span class="tf-ro-value">%s</span>' % labels[i])
elif input_type == "textarea":
R.append('<div class="tf-ro-textarea">%s</div>' % self.values[field])
elif input_type == "separator" or input_type == "hidden":
pass
elif input_type == "file":
R.append("'%s'" % self.values[field])
else:
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
if explanation:
R.append('<span class="tf-explanation">%s</span>' % explanation)
R.append("</td></tr>")
return "\n".join(R)
def _ReadOnlyVersion(self, formdescription):
"Generate HTML for read-only view of the form"
R = ['<table class="tf-ro">']
for (field, descr) in formdescription:
R.append(self._ReadOnlyElement(field, descr))
R.append("</table>")
return R
def dict2js(d):
"""convert Python dict to JS code"""
r = []
for k in d:
v = d[k]
if type(v) == BooleanType:
if v:
v = "true"
else:
v = "false"
elif type(v) == StringType:
v = '"' + v + '"'
r.append("%s: %s" % (k, v))
return "{" + ",\n".join(r) + "}"
def tf_error_message(msg):
"""html for form error message"""
if not msg:
return ""
if type(msg) == StringType:
msg = [msg]
return (
'<ul class="tf-msg"><li class="tf-msg">%s</li></ul>'
% '</li><li class="tf-msg">'.join(msg)
)

349
VERSION.py Normal file
View File

@ -0,0 +1,349 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "7.18"
SCONAME = "ScoDoc"
SCONEWS = """
<h4>Année 2020</h4>
<ul>
<li>Nouveau site <a href="https://scodoc.org">scodoc.org</a> pour la documentation.</li>
<li>Enregistrement de semestres extérieurs</li>
<li>Améliorations PV de Jury</li>
<li>Contributions J.-M. Place: aide au diagnostic problèmes export Apogée
</li>
</ul>
<h4>Année 2019</h4>
<ul>
<li>Support Linux Debian 10</li>
<li>Petites améliorations: import groupes, droits de suppression notes pour vacataires, etc.</li>
<li>Exports listes pour Moodle</li>
<li>Fonction facilitant l'envoi de données pour l'assistance technique</li>
</ul>
<h4>Année 2018</h4>
<ul>
<li>Affichage date finalisation inscription Apogée</li>
<li>Co-responsables de semestres</li>
<li>Amélioration page d'accueil département</li>
<li>Corrections diverses et petites améliorations</li>
<li>Avis de poursuites d'études plus robustes et configurables</li>
</ul>
<h4>Année 2017</h4>
<ul>
<li>Bonus/Malus sur les moyennes d'UE</li>
<li>Enregistrement des informations sur le devenir de l'étudiant</li>
<li>Export global des résultats d'un département (utile pour les rapports d'évaluation)</li>
<li>Compatibilité Linux Debian 9, et modernisation de certains composants</li>
<li>Génération des avis de poursuite d'études</li>
<li>Toilettage page liste programme pédagogiques</li>
<li>Amélioration exports résultats vers Apogée</li>
<li>Amélioration calcul des ECTS</li>
<li>Possibilité d'utilisation des adresses mail personnelles des étudiant</li>
<li>Corrections diverses</li>
</ul>
<h4>Année 2016</h4>
<ul>
<li>Import des données d'admissions depuis fichiers APB ou autres</li>
<li>Nouveau formulaire saisie des notes</li>
<li>Export des résultats vers Apogée pour un ensemble de semestre</li>
<li>Enregistrement du classement lors de l'admission</li>
<li>Modification du calcul des coefficients des UE capitalisées</li>
</ul>
<h4>Année 2015</h4>
<ul>
<li>Exports fichiers Apogée</li>
<li>Recherche étudiants sur plusieurs départements</li>
<li>Corrections diverses</li>
</ul>
<h4>Année 2014</h4>
<ul>
<li>Nouvelle interface pour listes groupes, photos, feuilles d'émargement.</li>
</ul>
<h4>Année 2013</h4>
<ul>
<li>Modernisation de nombreux composants logiciels (ScoDoc 7)</li>
<li>Saisie des absences par matières</li>
</ul>
<h4>Année 2012</h4>
<ul>
<li>Table lycées d'origine avec carte google</li>
<li>Amélioration des PV de jury (logos, ...)</li>
<li>Accélération du code de calcul des semestres</li>
<li>Changement documentation en ligne (nouveau site web)</li>
</ul>
<h4>Année 2011</h4>
<ul>
<li>Amélioration de la présentation des bulletins de notes, et possibilité de définir de nouveaux formats</li>
<li>Possibilité de modifier les moyennes d'UE via un "bonus" (sport/culture)</li>
<li>Ajout parcours spécifique pour UCAC (Cameroun)</li>
<li>Possibilité d'indiquer des mentions sur les PV</li>
<li>Evaluations de "rattrapage"</li>
<li>Support pour installation en Linux Debian "Squeeze"</li>
<li>Corrections diverses</li>
</ul>
<h4>Novembre 2010</h4>
<ul>
<li>Possibilité d'indiquer des évaluations avec publication immédiate des notes (même si incomplètes)</li>
</ul>
<h4>Octobre 2010</h4>
<ul>
<li>Nouvelle API JSON</li>
<li>Possibilité d'associer 2 étapes Apogée au même semestre</li>
<li>Table "poursuite études"</li>
<li>Possibilité d'envoyer un mail auto aux étudiants absentéistes<li>
</ul>
<h4>Août 2010</h4>
<ul>
<li>Définitions de parcours (DUT, LP, ...) avec prise en compte des spécificités (par ex., certaines barres d'UE différentes en LP)</li>
</ul>
<h4>Avril - Juin 2010</h4>
<ul>
<li>Formules utilisateur pour le calcul des moyennes d'UE</li>
<li>Nouveau système de notification des absences par mail</li>
<li>Affichage optionnel des valeurs mini et maxi des moyennes sur les bulletins</li>
<li>Nouveau code de décision jury semestre: "RAT" : en attente de rattrapage</li>
</ul>
<h4>Janvier 2010</h4>
<ul>
<li>Suivez l'actualité du développement sur Twitter: <a href="https://twitter.com/ScoDoc">@ScoDoc</a></li>
<li>Nouveau menu "Groupes" pour faciliter la prise en main</li>
<li>Possibilité de définir des règles ad hoc de calcul des moyennes de modules (formules)</li>
<li>Possibilité d'inclure des images (logos) dans les bulletins PDF</li>
<li>Bandeau "provisoire" sur les bulletins en cours de semestre</li>
<li>Possibilite de valider (capitaliser) une UE passee hors ScoDoc</li>
<li>Amelioration de l'édition des programmes (formations)</li>
<li>Nombreuses améliorations mineures</li>
</ul>
<h4>Novembre 2009</h4>
<ul>
<li>Gestion des partitions et groupes en nombres quelconques</li>
<li>Nouvelle gestion des photos</li>
<lI>Imports d'étudiants excel incrémentaux</li>
<li>Optimisations et petites améliorations</li>
</ul>
<h4>Septembre 2009</h4>
<ul>
<li>Traitement de "billets d'absences" (saisis par les étudiants sur le portail)</li>
</ul>
<h4>Juin 2009</h4>
<ul>
<li>Nouveau système plus flexibles de gestion des préférences (ou "paramètres")</li>
<li>Possiblité d'associer une nouvelle version de programme à un semestre</li>
<li>Corrections et améliorations diverses</h4>
</ul>
<h4>Juillet 2008: version 6.0</h4>
<ul>
<li>Installeur automatisé pour Linux</li>
<li>Amélioration ergonomie (barre menu pages semestres)</li>
<li>Refonte fiche étudiant (parcours)</li>
<li>Archivage des documents (PV)</li>
<li>Nouvel affichage des notes des évaluations</li>
<li>Nombreuses corrections et améliorations</li>
</ul>
<h4>Juin 2008</h4>
<ul>
<li>Rangs sur les bulletins</li>
</ul>
<h4>Février 2008</h4>
<ul>
<li>Statistiques et suivis de cohortes (chiffres et graphes)</li>
<li>Nombreuses petites corrections suites aux jurys de janvier</li>
</ul>
<h4>Janvier 2008</h4>
<ul>
<li>Personnalisation des régles de calculs notes d'option (sport, culture)</li>
<li>Edition de PV de jury individuel</li>
</ul>
<h4>Novembre 2007</h4>
<ul>
<li>Vérification des absences aux évaluations</li>
<li>Import des photos depuis portail, trombinoscopes en PDF</li>
</ul>
<h4>Septembre 2007</h4>
<ul>
<li>Importation des etudiants depuis étapes Apogée</li>
<li>Inscription de groupes à des modules (options ou parcours)</li>
<li>Listes de étapes Apogée (importées du portail)</li>
</ul>
<h4>Juillet 2007</h4>
<ul>
<li>Import utilisateurs depuis Excel</li>
<li>Nouvelle gestion des passage d'un semestre à l'autre</li>
</ul>
<h4>Juin 2007: version 5.0</h4>
<ul>
<li>Suivi des parcours et règles de décision des jurys DUT</li>
<li>Capitalisation des UEs</li>
<li>Edition des PV de jurys et courriers aux étudiants</li>
<li>Feuilles (excel) pour préparation jurys</li>
<li>Nombreuses petites améliorations</li>
</ul>
<h4>Avril 2007</h4>
<ul>
<li>Paramètres de mise en page des bulletins en PDF</li>
</ul>
<h4>Février 2007</h4>
<ul>
<li>Possibilité de ne <em>pas</em> publier les bulletins sur le portail</li>
<li>Gestion des notes "en attente" (publication d'évaluations sans correction de toutes les copies)</li>
<li>Amélioration formulaire saisie absences, saisie absences par semestre.</li>
</ul>
<h4>Janvier 2007</h4>
<ul>
<li>Possibilité d'initialiser les notes manquantes d'une évaluation</li>
<li>Recupération des codes NIP depuis Apogée</li>
<li>Gestion des compensations inter-semestre DUT (en cours de développement)</li>
<li>Export trombinoscope en archive zip</li>
</ul>
<h4>Octobre 2006</h4>
<ul>
<li>Réorganisation des pages d'accueil</li>
<li>Ajout des "nouvelles" (dernières opérations), avec flux RSS</li>
<li>Import/Export XML des formations, duplication d'une formation (versions)</li>
<li>Bulletins toujours sur une seule feuille (passage à ReportLab 2.0)</li>
<li>Suppression d'un utilisateur</il>
</ul>
<h4>Septembre 2006</h4>
<ul>
<li>Page pour suppression des groupes.</li>
<li>Amélioration gestion des utilisateurs</li>
<li>"Verrouillage" des semestres</li>
<li>Liste d'enseignants (chargés de TD) associés à un module (et pouvant saisir des notes)</li>
<li>Noms de types de groupes (TD, TP, ...) modifiables</li>
<li>Tableau rudimentaire donnant la répartition des bacs dans un semestre</li>
<li>Amélioration mise en page des listes au format excel</li>
<li>Annulation des démissions</li>
</ul>
<h4>Juillet 2006</h4>
<ul>
<li>Dialogue permettant au directeur des études de modifier
les options d'un semestre</li>
<li>Option pour ne pas afficher les UE validées sur les bulletins</li>
</ul>
<h4>30 juin 2006</h4>
<ul>
<li>Option pour ne pas afficher les décisions sur les bulletins</li>
<li>Génération feuilles pour préparation jury</li>
<li>Gestion des modules optionnels</li>
<li>Prise en compte note "activités culturelles ou sportives"</li>
<li>Amélioration tableau de bord semestre</li>
<li>Import listes étudiants depuis Excel (avec code Apogée)</li>
</ul>
<h4>12 juin 2006</h4>
<ul>
<li>Formulaire dynamique d'affectation aux groupes</li>
<li>Tri des tableaux (listes, récapitulatif)</li>
<li>Export XML des infos sur un etudiant et des groupes</li>
</ul>
<h4>12 mai 2006</h4>
<ul>
<li>Possibilité de suppression d'un semestre</li>
<li>Export XML du tableau recapitulatif des notes du semestre</li>
<li>Possibilité de supression d'une formation complète</li>
</ul>
<h4>24 avril 2006</h4>
<ul>
<li>Export bulletins en XML (expérimental)</li>
<li>Flag "gestion_absence" sur les semestres de formation</li>
</ul>
<h4>4 mars 2006</h4>
<ul>
<li>Formulaire d'inscription au semestre suivant.</li>
<li>Format "nombre" dans les feuilles excel exportées.</li>
</ul>
<h4>23 février 2006</h4>
<ul>
<li>Décisions jury sur bulletins.</li>
</ul>
<h4>17 janvier 2006</h4>
<ul>
<li>Ajout et édition d'appréciations sur les bulletins.</li>
</ul>
<h4>12 janvier 2006</h4>
<ul>
<li>Envoi des bulletins en PDF par mail aux étudiants.</li>
</ul>
<h4>6 janvier 2006</h4>
<ul>
<li>Affichage des ex-aequos.</li>
<li>Classeurs bulletins PDF en différentes versions.</li>
<li>Corrigé gestion des notes des démissionnaires.</li>
</ul>
<h4>1er janvier 2006</h4>
<ul>
<li>Import du projet dans Subversion / LIPN.</li>
<li>Lecture des feuilles de notes Excel.</li>
</ul>
<h4>31 décembre 2005</h4>
<ul>
<li>Listes générées au format Excel au lieu de CSV.</li>
<li>Bug fix (création/saisie evals).</li>
</ul>
<h4>29 décembre 2005</h4>
<ul>
<li>Affichage des moyennes de chaque groupe dans tableau de bord module.
</ul>
<h4>26 décembre 2005</h4>
<ul>
<li>Révision inscription/édition <em>individuelle</em> d'étudiants.</li>
<li>Amélioration fiche étudiant (cosmétique, liste formations, actions).</li>
<li>Listings notes d'évaluations anonymes (utilité douteuse ?).</li>
<li>Amélioration formulaire saisie notes ('enter' -> champ suivant).</li>
</ul>
<h4>24 décembre 2005</h4>
<ul>
<li>Génération de bulletins PDF
</li>
<li>Suppression de notes (permet donc de supprimer une évaluation)
</li>
<li>Bulletins en versions courtes (seulement moyennes de chaque module), longues
(toutes les notes) et intermédiaire (moyenne de chaque module plus notes dans les évaluations sélectionnées).
</li>
<li>Notes moyennes sous les barres en rouge dans le tableau récapitulatif (seuil=10 sur la moyenne générale, et 8 sur chaque UE).
</li>
<li>Colonne "groupe de TD" dans le tableau récapitulatif des notes.
</ul>
"""

2516
ZAbsences.py Normal file

File diff suppressed because it is too large Load Diff

898
ZEntreprises.py Normal file
View File

@ -0,0 +1,898 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2020 Emmanuel Viennet. All rights reserved.
#
# 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
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
""" Gestion des relations avec les entreprises
"""
import urllib
from sco_zope import *
# ---------------
from notesdb import *
from notes_log import log
from scolog import logdb
from sco_utils import *
import html_sidebar
from TrivialFormulator import TrivialFormulator, TF
import scolars
import string, re
import time, calendar
def _format_nom(nom):
"formatte nom (filtre en entree db) d'une entreprise"
if not nom:
return nom
nom = nom.decode(SCO_ENCODING)
return (nom[0].upper() + nom[1:]).encode(SCO_ENCODING)
class EntreprisesEditor(EditableTable):
def delete(self, cnx, oid):
"delete correspondants and contacts, then self"
# first, delete all correspondants and contacts
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
cursor.execute(
"delete from entreprise_contact where entreprise_id=%(entreprise_id)s",
{"entreprise_id": oid},
)
cursor.execute(
"delete from entreprise_correspondant where entreprise_id=%(entreprise_id)s",
{"entreprise_id": oid},
)
cnx.commit()
EditableTable.delete(self, cnx, oid)
def list(
self,
cnx,
args={},
operator="and",
test="=",
sortkey=None,
sort_on_contact=False,
ZEntrepriseInstance=None,
):
# list, then sort on date of last contact
R = EditableTable.list(
self, cnx, args=args, operator=operator, test=test, sortkey=sortkey
)
if sort_on_contact:
for r in R:
c = ZEntrepriseInstance.do_entreprise_contact_list(
args={"entreprise_id": r["entreprise_id"]}, disable_formatting=True
)
if c:
r["date"] = max([x["date"] or datetime.date.min for x in c])
else:
r["date"] = datetime.date.min
# sort
R.sort(lambda r1, r2: cmp(r2["date"], r1["date"]))
for r in R:
r["date"] = DateISOtoDMY(r["date"])
return R
def list_by_etud(
self, cnx, args={}, sort_on_contact=False, disable_formatting=False
):
"cherche rentreprise ayant eu contact avec etudiant"
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
cursor.execute(
"select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom",
args,
)
titles, res = [x[0] for x in cursor.description], cursor.dictfetchall()
R = []
for r in res:
r["etud_prenom"] = r["etud_prenom"] or ""
d = {}
for key in r:
v = r[key]
# format value
if not disable_formatting and self.output_formators.has_key(key):
v = self.output_formators[key](v)
d[key] = v
R.append(d)
# sort
if sort_on_contact:
R.sort(
lambda r1, r2: cmp(
r2["date"] or datetime.date.min, r1["date"] or datetime.date.min
)
)
for r in R:
r["date"] = DateISOtoDMY(r["date"] or datetime.date.min)
return R
_entreprisesEditor = EntreprisesEditor(
"entreprises",
"entreprise_id",
(
"entreprise_id",
"nom",
"adresse",
"ville",
"codepostal",
"pays",
"contact_origine",
"secteur",
"privee",
"localisation",
"qualite_relation",
"plus10salaries",
"note",
"date_creation",
),
sortkey="nom",
input_formators={"nom": _format_nom},
)
# ----------- Correspondants
_entreprise_correspEditor = EditableTable(
"entreprise_correspondant",
"entreprise_corresp_id",
(
"entreprise_corresp_id",
"entreprise_id",
"civilite",
"nom",
"prenom",
"fonction",
"phone1",
"phone2",
"mobile",
"fax",
"mail1",
"mail2",
"note",
),
sortkey="nom",
)
# ----------- Contacts
_entreprise_contactEditor = EditableTable(
"entreprise_contact",
"entreprise_contact_id",
(
"entreprise_contact_id",
"date",
"type_contact",
"entreprise_id",
"entreprise_corresp_id",
"etudid",
"description",
"enseignant",
),
sortkey="date",
output_formators={"date": DateISOtoDMY},
input_formators={"date": DateDMYtoISO},
)
# ---------------
class ZEntreprises(
ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
):
"ZEntreprises object"
meta_type = "ZEntreprises"
security = ClassSecurityInfo()
# This is the list of the methods associated to 'tabs' in the ZMI
# Be aware that The first in the list is the one shown by default, so if
# the 'View' tab is the first, you will never see your tabs by cliquing
# on the object.
manage_options = (
({"label": "Contents", "action": "manage_main"},)
+ PropertyManager.manage_options # add the 'Properties' tab
+ (
# this line is kept as an example with the files :
# dtml/manage_editZScolarForm.dtml
# html/ZScolar-edit.stx
# {'label': 'Properties', 'action': 'manage_editForm',},
{"label": "View", "action": "index_html"},
)
+ Item.manage_options # add the 'Undo' & 'Owner' tab
+ RoleManager.manage_options # add the 'Security' tab
)
# no permissions, only called from python
def __init__(self, id, title):
"initialise a new instance"
self.id = id
self.title = title
# The form used to edit this object
def manage_editZEntreprises(self, title, RESPONSE=None):
"Changes the instance values"
self.title = title
self._p_changed = 1
RESPONSE.redirect("manage_editForm")
# Ajout (dans l'instance) d'un dtml modifiable par Zope
def defaultDocFile(self, id, title, file):
f = open(file_path + "/dtml-editable/" + file + ".dtml")
file = f.read()
f.close()
self.manage_addDTMLMethod(id, title, file)
security.declareProtected(ScoEntrepriseView, "entreprise_header")
def entreprise_header(self, REQUEST=None, page_title=""):
"common header for all Entreprises pages"
authuser = REQUEST.AUTHENTICATED_USER
# _read_only is used to modify pages properties (links, buttons)
# Python methods (do_xxx in this class) are also protected individualy)
if authuser.has_permission(ScoEntrepriseChange, self):
REQUEST.set("_read_only", False)
else:
REQUEST.set("_read_only", True)
return self.sco_header(REQUEST, container=self, page_title=page_title)
security.declareProtected(ScoEntrepriseView, "entreprise_footer")
def entreprise_footer(self, REQUEST):
"common entreprise footer"
return self.sco_footer(REQUEST)
security.declareProtected(ScoEntrepriseView, "sidebar")
def sidebar(self, REQUEST):
"barre gauche (overide std sco sidebar)"
# rewritten from legacy DTML code
context = self
params = {"ScoURL": context.ScoURL()}
H = [
"""<div id="sidebar-container">
<div class="sidebar">""",
html_sidebar.sidebar_common(context, REQUEST),
"""<h2 class="insidebar"><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a></h2>
<ul class="insidebar">"""
% params,
]
if not REQUEST["_read_only"]:
H.append(
"""<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_create" class="sidebar">Nouvelle entreprise</a> </li>"""
% params
)
H.append(
"""<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_list" class="sidebar">Contacts</a> </li></ul> """
% params
)
# --- entreprise selectionnée:
if REQUEST.form.has_key("entreprise_id"):
entreprise_id = REQUEST.form["entreprise_id"]
E = context.do_entreprise_list(args={"entreprise_id": entreprise_id})
if E:
E = E[0]
params.update(E)
H.append(
"""<div class="entreprise-insidebar">
<h3 class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_edit?entreprise_id=%(entreprise_id)s" class="sidebar">%(nom)s</a></h2>
<ul class="insidebar">
<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_correspondant_list?entreprise_id=%(entreprise_id)s" class="sidebar">Corresp.</a></li>"""
% params
) # """
if not REQUEST["_read_only"]:
H.append(
"""<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_correspondant_create?entreprise_id=%(entreprise_id)s" class="sidebar">Nouveau Corresp.</a></li>"""
% params
)
H.append(
"""<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_list?entreprise_id=%(entreprise_id)s" class="sidebar">Contacts</a></li>"""
% params
)
if not REQUEST["_read_only"]:
H.append(
"""<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_create?entreprise_id=%(entreprise_id)s" class="sidebar">Nouveau "contact"</a></li>"""
% params
)
H.append("</ul></div>")
#
H.append("""<br/><br/>%s""" % icontag("entreprise_side_img"))
if REQUEST["_read_only"]:
H.append("""<br/><em>(Lecture seule)</em>""")
H.append("""</div> </div> <!-- end of sidebar -->""")
return "".join(H)
# --------------------------------------------------------------------
#
# Entreprises : Methodes en DTML
#
# --------------------------------------------------------------------
# used to view content of the object
security.declareProtected(ScoEntrepriseView, "index_html")
index_html = DTMLFile("dtml/entreprises/index_html", globals())
security.declareProtected(ScoEntrepriseView, "entreprise_contact_list")
entreprise_contact_list = DTMLFile(
"dtml/entreprises/entreprise_contact_list", globals()
)
security.declareProtected(ScoEntrepriseView, "entreprise_correspondant_list")
entreprise_correspondant_list = DTMLFile(
"dtml/entreprises/entreprise_correspondant_list", globals()
)
# les methodes "edit" sont aussi en ScoEntrepriseView car elles permettent
# la visualisation (via variable _read_only positionnee dans entreprise_header)
security.declareProtected(ScoEntrepriseView, "entreprise_contact_edit")
entreprise_contact_edit = DTMLFile(
"dtml/entreprises/entreprise_contact_edit", globals()
)
security.declareProtected(ScoEntrepriseView, "entreprise_correspondant_edit")
entreprise_correspondant_edit = DTMLFile(
"dtml/entreprises/entreprise_correspondant_edit", globals()
)
# Acces en modification:
security.declareProtected(ScoEntrepriseChange, "entreprise_contact_create")
entreprise_contact_create = DTMLFile(
"dtml/entreprises/entreprise_contact_create", globals()
)
security.declareProtected(ScoEntrepriseChange, "entreprise_contact_delete")
entreprise_contact_delete = DTMLFile(
"dtml/entreprises/entreprise_contact_delete", globals()
)
security.declareProtected(ScoEntrepriseChange, "entreprise_correspondant_create")
entreprise_correspondant_create = DTMLFile(
"dtml/entreprises/entreprise_correspondant_create", globals()
)
security.declareProtected(ScoEntrepriseChange, "entreprise_correspondant_delete")
entreprise_correspondant_delete = DTMLFile(
"dtml/entreprises/entreprise_correspondant_delete", globals()
)
security.declareProtected(ScoEntrepriseChange, "entreprise_delete")
entreprise_delete = DTMLFile("dtml/entreprises/entreprise_delete", globals())
# --------------------------------------------------------------------
#
# Entreprises : Methodes en Python
#
# --------------------------------------------------------------------
security.declareProtected(ScoEntrepriseChange, "do_entreprise_create")
def do_entreprise_create(self, args):
"entreprise_create"
cnx = self.GetDBConnexion()
r = _entreprisesEditor.create(cnx, args)
return r
security.declareProtected(ScoEntrepriseChange, "do_entreprise_delete")
def do_entreprise_delete(self, oid):
"entreprise_delete"
cnx = self.GetDBConnexion()
_entreprisesEditor.delete(cnx, oid)
security.declareProtected(ScoEntrepriseView, "do_entreprise_list")
def do_entreprise_list(self, **kw):
"entreprise_list"
cnx = self.GetDBConnexion()
kw["ZEntrepriseInstance"] = self
return _entreprisesEditor.list(cnx, **kw)
security.declareProtected(ScoEntrepriseView, "do_entreprise_list_by_etud")
def do_entreprise_list_by_etud(self, **kw):
"entreprise_list_by_etud"
cnx = self.GetDBConnexion()
return _entreprisesEditor.list_by_etud(cnx, **kw)
security.declareProtected(ScoEntrepriseView, "do_entreprise_edit")
def do_entreprise_edit(self, *args, **kw):
"entreprise_edit"
cnx = self.GetDBConnexion()
_entreprisesEditor.edit(cnx, *args, **kw)
security.declareProtected(ScoEntrepriseChange, "do_entreprise_correspondant_create")
def do_entreprise_correspondant_create(self, args):
"entreprise_correspondant_create"
cnx = self.GetDBConnexion()
r = _entreprise_correspEditor.create(cnx, args)
return r
security.declareProtected(ScoEntrepriseChange, "do_entreprise_correspondant_delete")
def do_entreprise_correspondant_delete(self, oid):
"entreprise_correspondant_delete"
cnx = self.GetDBConnexion()
_entreprise_correspEditor.delete(cnx, oid)
security.declareProtected(ScoEntrepriseView, "do_entreprise_correspondant_list")
def do_entreprise_correspondant_list(self, **kw):
"entreprise_correspondant_list"
cnx = self.GetDBConnexion()
return _entreprise_correspEditor.list(cnx, **kw)
security.declareProtected(ScoEntrepriseView, "do_entreprise_correspondant_edit")
def do_entreprise_correspondant_edit(self, *args, **kw):
"entreprise_correspondant_edit"
cnx = self.GetDBConnexion()
_entreprise_correspEditor.edit(cnx, *args, **kw)
def do_entreprise_correspondant_listnames(self, args={}):
"-> liste des noms des correspondants (pour affichage menu)"
cnx = self.GetDBConnexion()
C = self.do_entreprise_correspondant_list(args=args)
return [
(x["prenom"] + " " + x["nom"], str(x["entreprise_corresp_id"])) for x in C
]
security.declareProtected(ScoEntrepriseChange, "do_entreprise_contact_create")
def do_entreprise_contact_create(self, args):
"entreprise_contact_create"
cnx = self.GetDBConnexion()
r = _entreprise_contactEditor.create(cnx, args)
return r
security.declareProtected(ScoEntrepriseChange, "do_entreprise_contact_delete")
def do_entreprise_contact_delete(self, oid):
"entreprise_contact_delete"
cnx = self.GetDBConnexion()
_entreprise_contactEditor.delete(cnx, oid)
security.declareProtected(ScoEntrepriseView, "do_entreprise_contact_list")
def do_entreprise_contact_list(self, **kw):
"entreprise_contact_list"
cnx = self.GetDBConnexion()
return _entreprise_contactEditor.list(cnx, **kw)
security.declareProtected(ScoEntrepriseView, "do_entreprise_contact_edit")
def do_entreprise_contact_edit(self, *args, **kw):
"entreprise_contact_edit"
cnx = self.GetDBConnexion()
_entreprise_contactEditor.edit(cnx, *args, **kw)
#
security.declareProtected(ScoEntrepriseView, "do_entreprise_check_etudiant")
def do_entreprise_check_etudiant(self, etudiant):
"""Si etudiant est vide, ou un ETUDID valide, ou un nom unique,
retourne (1, ETUDID).
Sinon, retourne (0, 'message explicatif')
"""
etudiant = etudiant.strip().translate(
None, "'()"
) # suppress parens and quote from name
if not etudiant:
return 1, None
cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
cursor.execute(
"select etudid, nom, prenom from identite where upper(nom) ~ upper(%(etudiant)s) or etudid=%(etudiant)s",
{"etudiant": etudiant},
)
r = cursor.fetchall()
if len(r) < 1:
return 0, 'Aucun etudiant ne correspond à "%s"' % etudiant
elif len(r) > 10:
return (
0,
"<b>%d etudiants</b> correspondent à ce nom (utilisez le code)"
% len(r),
)
elif len(r) > 1:
e = ['<ul class="entreprise_etud_list">']
for x in r:
e.append(
"<li>%s %s (code %s)</li>"
% (strupper(x[1]), x[2] or "", x[0].strip())
)
e.append("</ul>")
return (
0,
"Les étudiants suivants correspondent: préciser le nom complet ou le code\n"
+ "\n".join(e),
)
else: # une seule reponse !
return 1, r[0][0].strip()
security.declareProtected(ScoEntrepriseView, "do_entreprise_list_by_contact")
def do_entreprise_list_by_contact(
self, args={}, operator="and", test="=", sortkey=None
):
"""Recherche dans entreprises, avec date de contact"""
# (fonction ad-hoc car requete sur plusieurs tables)
raise NotImplementedError
# XXXXX fonction non achevee , non testee...
cnx = self.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
vals = dictfilter(args, self.dbfields)
# DBSelect
what = ["*"]
operator = " " + operator + " "
cond = " E.entreprise_id = C.entreprise_id "
if vals:
cond += " where " + operator.join(
["%s%s%%(%s)s" % (x, test, x) for x in vals.keys() if vals[x] != None]
)
cnuls = " and ".join(
["%s is NULL" % x for x in vals.keys() if vals[x] is None]
)
if cnuls:
cond = cond + " and " + cnuls
else:
cond += ""
cursor.execute(
"select distinct"
+ ", ".join(what)
+ " from entreprises E, entreprise_contact C "
+ cond
+ orderby,
vals,
)
titles, res = [x[0] for x in cursor.description], cursor.fetchall()
#
R = []
for r in res:
d = {}
for i in range(len(titles)):
v = r[i]
# value not formatted ! (see EditableTable.list())
d[titles[i]] = v
R.append(d)
return R
# -------- Formulaires: traductions du DTML
security.declareProtected(ScoEntrepriseChange, "entreprise_create")
def entreprise_create(self, REQUEST=None):
"""Form. création entreprise"""
context = self
H = [
self.entreprise_header(REQUEST, page_title="Création d'une entreprise"),
"""<h2 class="entreprise_new">Création d'une entreprise</h2>""",
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
(
("nom", {"size": 25, "title": "Nom de l'entreprise"}),
(
"adresse",
{"size": 30, "title": "Adresse", "explanation": "(numéro, rue)"},
),
("codepostal", {"size": 8, "title": "Code Postal"}),
("ville", {"size": 30, "title": "Ville"}),
("pays", {"size": 30, "title": "Pays", "default": "France"}),
(
"localisation",
{
"input_type": "menu",
"labels": ["Ile de France", "Province", "Etranger"],
"allowed_values": ["IDF", "Province", "Etranger"],
},
),
("secteur", {"size": 30, "title": "Secteur d'activités"}),
(
"privee",
{
"input_type": "menu",
"title": "Statut",
"labels": [
"Entreprise privee",
"Entreprise Publique",
"Association",
],
"allowed_values": ["privee", "publique", "association"],
},
),
(
"plus10salaries",
{
"title": "Masse salariale",
"type": "integer",
"input_type": "menu",
"labels": [
"10 salariés ou plus",
"Moins de 10 salariés",
"Inconnue",
],
"allowed_values": [1, 0, -1],
},
),
(
"qualite_relation",
{
"title": "Qualité relation IUT/Entreprise",
"input_type": "menu",
"default": "-1",
"labels": [
"Très bonne",
"Bonne",
"Moyenne",
"Mauvaise",
"Inconnue",
],
"allowed_values": ["100", "75", "50", "25", "-1"],
},
),
("contact_origine", {"size": 30, "title": "Origine du contact"}),
(
"note",
{"input_type": "textarea", "rows": 3, "cols": 40, "title": "Note"},
),
),
cancelbutton="Annuler",
submitlabel="Ajouter cette entreprise",
readonly=REQUEST["_read_only"],
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + context.entreprise_footer(REQUEST)
elif tf[0] == -1:
return REQUEST.RESPONSE.redirect(REQUEST.URL1)
else:
self.do_entreprise_create(tf[2])
return REQUEST.RESPONSE.redirect(REQUEST.URL1)
security.declareProtected(ScoEntrepriseView, "entreprise_edit")
def entreprise_edit(self, entreprise_id, REQUEST=None, start=1):
"""Form. edit entreprise"""
context = self
authuser = REQUEST.AUTHENTICATED_USER
readonly = not authuser.has_permission(ScoEntrepriseChange, self)
F = self.do_entreprise_list(args={"entreprise_id": entreprise_id})[0]
H = [
self.entreprise_header(REQUEST, page_title="Entreprise"),
"""<h2 class="entreprise">%(nom)s</h2>""" % F,
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
(
("entreprise_id", {"default": entreprise_id, "input_type": "hidden"}),
("start", {"default": 1, "input_type": "hidden"}),
(
"date_creation",
{"default": time.strftime("%Y-%m-%d"), "input_type": "hidden"},
),
("nom", {"size": 25, "title": "Nom de l'entreprise"}),
(
"adresse",
{"size": 30, "title": "Adresse", "explanation": "(numéro, rue)"},
),
("codepostal", {"size": 8, "title": "Code Postal"}),
("ville", {"size": 30, "title": "Ville"}),
("pays", {"size": 30, "title": "Pays", "default": "France"}),
(
"localisation",
{
"input_type": "menu",
"labels": ["Ile de France", "Province", "Etranger"],
"allowed_values": ["IDF", "Province", "Etranger"],
},
),
("secteur", {"size": 30, "title": "Secteur d'activités"}),
(
"privee",
{
"input_type": "menu",
"title": "Statut",
"labels": [
"Entreprise privee",
"Entreprise Publique",
"Association",
],
"allowed_values": ["privee", "publique", "association"],
},
),
(
"plus10salaries",
{
"title": "Masse salariale",
"input_type": "menu",
"labels": [
"10 salariés ou plus",
"Moins de 10 salariés",
"Inconnue",
],
"allowed_values": ["1", "0", "-1"],
},
),
(
"qualite_relation",
{
"title": "Qualité relation IUT/Entreprise",
"input_type": "menu",
"labels": [
"Très bonne",
"Bonne",
"Moyenne",
"Mauvaise",
"Inconnue",
],
"allowed_values": ["100", "75", "50", "25", "-1"],
},
),
("contact_origine", {"size": 30, "title": "Origine du contact"}),
(
"note",
{"input_type": "textarea", "rows": 3, "cols": 40, "title": "Note"},
),
),
cancelbutton="Annuler",
initvalues=F,
submitlabel="Modifier les valeurs",
readonly=readonly,
)
if tf[0] == 0:
H.append(tf[1])
Cl = self.do_entreprise_correspondant_list(
args={"entreprise_id": F["entreprise_id"]}
)
Cts = self.do_entreprise_contact_list(
args={"entreprise_id": F["entreprise_id"]}
)
if not readonly:
H.append(
"""<p>%s&nbsp;<a class="entreprise_delete" href="entreprise_delete?entreprise_id=%s">Supprimer cette entreprise</a> </p>"""
% (
icontag("delete_img", title="delete", border="0"),
F["entreprise_id"],
)
)
if len(Cl):
H.append(
"""<h3>%d correspondants dans l'entreprise %s (<a href="entreprise_correspondant_list?entreprise_id=%s">liste complète</a>) :</h3>
<ul>"""
% (len(Cl), F["nom"], F["entreprise_id"])
)
for c in Cl:
H.append(
"""<li><a href="entreprise_correspondant_edit?entreprise_corresp_id=%s">"""
% c["entreprise_corresp_id"]
)
if c["nom"]:
nom = (
c["nom"]
.decode(SCO_ENCODING)
.lower()
.capitalize()
.encode(SCO_ENCODING)
)
else:
nom = ""
if c["prenom"]:
prenom = (
c["prenom"]
.decode(SCO_ENCODING)
.lower()
.capitalize()
.encode(SCO_ENCODING)
)
else:
prenom = ""
H.append(
"""%s %s</a>&nbsp;(%s)</li>""" % (nom, prenom, c["fonction"])
)
H.append("</ul>")
if len(Cts):
H.append(
"""<h3>%d contacts avec l'entreprise %s (<a href="entreprise_contact_list?entreprise_id=%s">liste complète</a>) :</h3><ul>"""
% (len(Cts), F["nom"], F["entreprise_id"])
)
for c in Cts:
H.append(
"""<li><a href="entreprise_contact_edit?entreprise_contact_id=%s">%s</a>&nbsp;&nbsp;&nbsp;"""
% (c["entreprise_contact_id"], c["date"])
)
if c["type_contact"]:
H.append(c["type_contact"])
if c["etudid"]:
etud = self.getEtudInfo(etudid=c["etudid"], filled=1)
if etud:
etud = etud[0]
H.append(
"""<a href="%s/ficheEtud?etudid=%s">%s</a>"""
% (self.ScoURL(), c["etudid"], etud["nomprenom"])
)
if c["description"]:
H.append("(%s)" % c["description"])
H.append("</li>")
H.append("</ul>")
return "\n".join(H) + context.entreprise_footer(REQUEST)
elif tf[0] == -1:
return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "?start=" + start)
else:
self.do_entreprise_edit(tf[2])
return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "?start=" + start)
# --- Misc tools.... ------------------
security.declareProtected(ScoEntrepriseView, "str_abbrev")
def str_abbrev(self, s, maxlen):
"abreviation"
if s == None:
return "?"
if len(s) < maxlen:
return s
return s[: maxlen - 3] + "..."
security.declareProtected(ScoEntrepriseView, "setPageSizeCookie")
def setPageSizeCookie(self, REQUEST=None):
"set page size cookie"
RESPONSE = REQUEST.RESPONSE
#
if REQUEST.form.has_key("entreprise_page_size"):
RESPONSE.setCookie(
"entreprise_page_size",
REQUEST.form["entreprise_page_size"],
path="/",
expires="Wed, 31-Dec-2025 23:55:00 GMT",
)
RESPONSE.redirect(REQUEST.form["target_url"])
security.declareProtected(ScoEntrepriseView, "make_link_create_corr")
def make_link_create_corr(self, entreprise_id):
"yet another stupid code snippet"
return (
'<a href="entreprise_correspondant_create?entreprise_id='
+ str(entreprise_id)
+ '">créer un nouveau correspondant</a>'
)
# --------------------------------------------------------------------
#
# Zope Product Administration
#
# --------------------------------------------------------------------
def manage_addZEntreprises(
self, id="id_ZEntreprises", title="The Title for ZEntreprises Object", REQUEST=None
):
"Add a ZEntreprises instance to a folder."
self._setObject(id, ZEntreprises(id, title))
if REQUEST is not None:
return self.manage_main(self, REQUEST)
# return self.manage_editForm(self, REQUEST)
# The form used to get the instance id from the user.
# manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals())

3647
ZNotes.py Normal file

File diff suppressed because it is too large Load Diff

954
ZScoDoc.py Normal file
View File

@ -0,0 +1,954 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2020 Emmanuel Viennet. All rights reserved.
#
# 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
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Site ScoDoc pour plusieurs departements:
gestion de l'installation et des creation de départements.
Chaque departement est géré par un ZScolar sous ZScoDoc.
"""
import time, string, glob, re, inspect
import urllib, urllib2, cgi, xml
try:
from cStringIO import StringIO
except:
from StringIO import StringIO
from zipfile import ZipFile
import os.path, glob
import traceback
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email.Header import Header
from email import Encoders
from sco_zope import *
#
try:
import Products.ZPsycopgDA.DA as ZopeDA
except:
import ZPsycopgDA.DA as ZopeDA # interp.py
from sco_utils import *
from notes_log import log
import sco_find_etud
from ZScoUsers import pwdFascistCheck
class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):
"ZScoDoc object"
meta_type = "ZScoDoc"
security = ClassSecurityInfo()
file_path = Globals.package_home(globals())
# This is the list of the methods associated to 'tabs' in the ZMI
# Be aware that The first in the list is the one shown by default, so if
# the 'View' tab is the first, you will never see your tabs by cliquing
# on the object.
manage_options = (
({"label": "Contents", "action": "manage_main"},)
+ PropertyManager.manage_options # add the 'Properties' tab
+ (
# this line is kept as an example with the files :
# dtml/manage_editZScolarForm.dtml
# html/ZScolar-edit.stx
# {'label': 'Properties', 'action': 'manage_editForm',},
{"label": "View", "action": "index_html"},
)
+ Item.manage_options # add the 'Undo' & 'Owner' tab
+ RoleManager.manage_options # add the 'Security' tab
)
def __init__(self, id, title):
"Initialise a new instance of ZScoDoc"
self.id = id
self.title = title
self.manage_addProperty("admin_password_initialized", "0", "string")
security.declareProtected(ScoView, "ScoDocURL")
def ScoDocURL(self):
"base URL for this instance (top level for ScoDoc site)"
return self.absolute_url()
def _check_admin_perm(self, REQUEST):
"""Check if user has permission to add/delete departements
"""
authuser = REQUEST.AUTHENTICATED_USER
if authuser.has_role("manager") or authuser.has_permission(ScoSuperAdmin, self):
return ""
else:
return """<h2>Vous n'avez pas le droit d'accéder à cette page</h2>"""
def _check_users_folder(self, REQUEST=None):
"""Vérifie UserFolder et le crée s'il le faut
"""
try:
udb = self.UsersDB
return "<!-- uf ok -->"
except:
e = self._check_admin_perm(REQUEST)
if not e: # admin permissions:
self.create_users_cnx(REQUEST)
self.create_users_folder(REQUEST)
return '<div class="head_message">Création du connecteur utilisateurs réussie</div>'
else:
return """<div class="head_message">Installation non terminée: connectez vous avec les droits d'administrateur</div>"""
security.declareProtected("View", "create_users_folder")
def create_users_folder(self, REQUEST=None):
"""Create Zope user folder
"""
e = self._check_admin_perm(REQUEST)
if e:
return e
if REQUEST is None:
REQUEST = {}
REQUEST.form["pgauth_connection"] = "UsersDB"
REQUEST.form["pgauth_table"] = "sco_users"
REQUEST.form["pgauth_usernameColumn"] = "user_name"
REQUEST.form["pgauth_passwordColumn"] = "passwd"
REQUEST.form["pgauth_rolesColumn"] = "roles"
add_method = self.manage_addProduct["OFSP"].manage_addexUserFolder
log("create_users_folder: in %s" % self.id)
return add_method(
authId="pgAuthSource",
propId="nullPropSource",
memberId="nullMemberSource",
groupId="nullGroupSource",
cryptoId="MD51",
# doAuth='1', doProp='1', doMember='1', doGroup='1', allDone='1',
cookie_mode=2,
session_length=500,
not_session_length=0,
REQUEST=REQUEST,
)
def _fix_users_folder(self):
"""removes docLogin and docLogout dtml methods from exUserFolder, so that we use ours.
(called each time be index_html, to fix old ScoDoc installations.)
"""
try:
self.acl_users.manage_delObjects(ids=["docLogin", "docLogout"])
except:
pass
# add missing getAuthFailedMessage (bug in exUserFolder ?)
try:
x = self.getAuthFailedMessage
except:
log("adding getAuthFailedMessage to Zope install")
parent = self.aq_parent
from OFS.DTMLMethod import addDTMLMethod
addDTMLMethod(parent, "getAuthFailedMessage", file="Identification")
security.declareProtected("View", "create_users_cnx")
def create_users_cnx(self, REQUEST=None):
"""Create Zope connector to UsersDB
Note: la connexion est fixée (SCOUSERS) (base crée par l'installeur) !
Les utilisateurs avancés pourront la changer ensuite.
"""
# ce connecteur zope - db est encore pour l'instant utilisé par exUserFolder.pgAuthSource
# (en lecture seule en principe)
oid = "UsersDB"
log("create_users_cnx: in %s" % self.id)
da = ZopeDA.Connection(
oid,
"Cnx bd utilisateurs",
SCO_DEFAULT_SQL_USERS_CNX,
False,
check=1,
tilevel=2,
encoding="LATIN1",
)
self._setObject(oid, da)
security.declareProtected("View", "change_admin_user")
def change_admin_user(self, password, REQUEST=None):
"""Change password of admin user"""
# note: controle sur le role et non pas sur une permission
# (non definies au top level)
if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
log("user %s is not Manager" % REQUEST.AUTHENTICATED_USER)
log("roles=%s" % REQUEST.AUTHENTICATED_USER.getRolesInContext(self))
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
log("trying to change admin password")
# 1-- check strong password
if pwdFascistCheck(password) != None:
log("refusing weak password")
return REQUEST.RESPONSE.redirect(
"change_admin_user_form?message=Mot%20de%20passe%20trop%20simple,%20recommencez"
)
# 2-- change password for admin user
username = "admin"
acl_users = self.aq_parent.acl_users
user = acl_users.getUser(username)
r = acl_users._changeUser(
username, password, password, user.roles, user.domains
)
if not r:
# OK, set property to indicate we changed the password
log("admin password changed successfully")
self.manage_changeProperties(admin_password_initialized="1")
return r or REQUEST.RESPONSE.redirect("index_html")
security.declareProtected("View", "change_admin_user_form")
def change_admin_user_form(self, message="", REQUEST=None):
"""Form allowing to change the ScoDoc admin password"""
# note: controle sur le role et non pas sur une permission
# (non definies au top level)
if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
H = [
self.scodoc_top_html_header(
REQUEST, page_title="ScoDoc: changement mot de passe"
)
]
if message:
H.append('<div id="message">%s</div>' % message)
H.append(
"""<h2>Changement du mot de passe administrateur (utilisateur admin)</h2>
<p>
<form action="change_admin_user" method="post"><table>
<tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
<tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
</table>
<input type="submit" value="Changer">
"""
)
H.append("""</body></html>""")
return "\n".join(H)
security.declareProtected("View", "list_depts")
def list_depts(self, REQUEST=None):
"""List departments folders
(returns a list of Zope folders containing a ZScolar instance)
"""
folders = self.objectValues("Folder")
# select folders with Scolarite object:
r = []
for folder in folders:
try:
s = folder.Scolarite
r.append(folder)
except:
pass
return r
security.declareProtected("View", "create_dept")
def create_dept(self, REQUEST=None, DeptId="", pass2=False):
"""Creation (ajout) d'un site departement
(instance ZScolar + dossier la contenant)
"""
e = self._check_admin_perm(REQUEST)
if e:
return e
if not DeptId:
raise ValueError("nom de departement invalide")
if not pass2:
# 1- Creation de repertoire Dept
add_method = self.manage_addProduct["OFSP"].manage_addFolder
add_method(DeptId, title="Site dept. " + DeptId)
DeptFolder = self[DeptId]
if not pass2:
# 2- Creation du repertoire Fotos
add_method = DeptFolder.manage_addProduct["OFSP"].manage_addFolder
add_method("Fotos", title="Photos identites " + DeptId)
# 3- Creation instance ScoDoc
add_method = DeptFolder.manage_addProduct["ScoDoc"].manage_addZScolarForm
return add_method(DeptId, REQUEST=REQUEST)
security.declareProtected("View", "delete_dept")
def delete_dept(self, REQUEST=None, DeptId="", force=False):
"""Supprime un departement (de Zope seulement, ne touche pas la BD)
"""
e = self._check_admin_perm(REQUEST)
if e:
return e
if not force and DeptId not in [x.id for x in self.list_depts()]:
raise ValueError("nom de departement invalide")
self.manage_delObjects(ids=[DeptId])
return (
"<p>Département "
+ DeptId
+ """ supprimé du serveur web (la base de données n'est pas affectée)!</p><p><a href="%s">Continuer</a></p>"""
% REQUEST.URL1
)
_top_level_css = """
<style type="text/css">
</style>"""
_html_begin = """<?xml version="1.0" encoding="%(encoding)s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%(page_title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript">
window.onload=function(){enableTooltips("gtrcontent")};
</script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/etud_info.js"></script>
"""
def scodoc_top_html_header(self, REQUEST, page_title="ScoDoc"):
H = [
self._html_begin
% {"page_title": "ScoDoc: bienvenue", "encoding": SCO_ENCODING},
self._top_level_css,
"""</head><body class="gtrcontent" id="gtrcontent">""",
CUSTOM_HTML_HEADER_CNX,
]
return "\n".join(H)
security.declareProtected("View", "index_html")
def index_html(self, REQUEST=None, message=None):
"""Top level page for ScoDoc
"""
authuser = REQUEST.AUTHENTICATED_USER
deptList = self.list_depts()
self._fix_users_folder() # fix our exUserFolder
isAdmin = not self._check_admin_perm(REQUEST)
try:
admin_password_initialized = self.admin_password_initialized
except:
admin_password_initialized = "0"
if isAdmin and admin_password_initialized != "1":
REQUEST.RESPONSE.redirect(
"ScoDoc/change_admin_user_form?message=Le%20mot%20de%20passe%20administrateur%20doit%20etre%20change%20!"
)
# Si l'URL indique que l'on est dans un folder, affiche page login du departement
try:
deptfoldername = REQUEST.URL0.split("ScoDoc")[1].split("/")[1]
if deptfoldername in [x.id for x in self.list_depts()]:
return self.index_dept(deptfoldername=deptfoldername, REQUEST=REQUEST)
except:
pass
H = [
self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"),
self._check_users_folder(REQUEST=REQUEST), # ensure setup is done
]
if message:
H.append('<div id="message">%s</div>' % message)
if isAdmin and not message:
H.append('<div id="message">Attention: connecté comme administrateur</div>')
H.append(
"""
<div class="maindiv">
<h2>ScoDoc: gestion scolarité</h2>
"""
)
if authuser.has_role("Authenticated"):
H.append(
"""<p>Bonjour <font color="red"><b>%s</b></font>.</p>""" % str(authuser)
)
H.append(
"""<p>N'oubliez pas de vous <a href="acl_users/logout">déconnecter</a> après usage.</p>"""
)
else:
H.append(
"""<p>Ce site est <font color="red"><b>réservé au personnel autorisé</b></font></p>"""
)
H.append(self.authentication_form(destination="."))
if not deptList:
H.append("<em>aucun département existant !</em>")
# si pas de dept et pas admin, propose lien pour loger admin
if not isAdmin:
H.append(
"""<p><a href="/force_admin_authentication">Identifiez vous comme administrateur</a> (au début: nom 'admin', mot de passe 'scodoc')</p>"""
)
else:
H.append('<ul class="main">')
if isAdmin:
dest_folder = "/Scolarite"
else:
dest_folder = ""
for deptFolder in self.list_depts():
if authuser.has_permission(ScoView, deptFolder.Scolarite):
link_cls = "link_accessible"
else:
link_cls = "link_unauthorized"
# Essai de recuperer le nom du departement dans ses preferences
try:
DeptName = (
deptFolder.Scolarite.get_preference("DeptName") or deptFolder.id
)
except:
DeptName = deptFolder.id
H.append(
'<li><a class="stdlink %s" href="%s%s">Département %s</a>'
% (link_cls, deptFolder.absolute_url(), dest_folder, DeptName)
)
# check if roles are initialized in this depts, and do it if necessary
if deptFolder.Scolarite.roles_initialized == "0":
if isAdmin:
deptFolder.Scolarite._setup_initial_roles_and_permissions()
else:
H.append(" (non initialisé, connectez vous comme admin)")
H.append("</li>")
H.append("</ul>")
# Recherche etudiant
H.append(sco_find_etud.form_search_etud_in_accessible_depts(self, REQUEST))
if isAdmin:
H.append('<p><a href="scodoc_admin">Administration de ScoDoc</a></p>')
else:
H.append(
'<p><a href="%s/force_admin_authentication">Se connecter comme administrateur</a></p>'
% REQUEST.BASE0
)
H.append(
"""
<div id="scodoc_attribution">
<p><a href="%s">ScoDoc</a> est un logiciel libre de suivi de la scolarité des étudiants conçu par
E. Viennet (Université Paris 13).</p>
</div>
</div>"""
% (SCO_WEBSITE,)
)
H.append("""</body></html>""")
return "\n".join(H)
def authentication_form(self, destination=""):
"""html snippet for authentication"""
return (
"""<!-- authentication_form -->
<form action="doLogin" method="post">
<input type="hidden" name="destination" value="%s"/>
<p>
<table border="0" cellpadding="3">
<tr>
<td><b>Nom:</b></td>
<td><input id="name" type="text" name="__ac_name" size="20"/></td>
</tr><tr>
<td><b>Mot de passe:</b></td>
<td><input id="password" type="password" name="__ac_password" size="20"/></td>
<td><input id="submit" name="submit" type="submit" value="OK"/></td>
</tr>
</table>
</p>
</form>"""
% destination
)
security.declareProtected("View", "index_dept")
def index_dept(self, deptfoldername="", REQUEST=None):
"""Page d'accueil departement"""
authuser = REQUEST.AUTHENTICATED_USER
try:
dept = getattr(self, deptfoldername)
if authuser.has_permission(ScoView, dept):
return REQUEST.RESPONSE.redirect("ScoDoc/%s/Scolarite" % deptfoldername)
except:
log(
"*** problem in index_dept (%s) user=%s"
% (deptfoldername, str(authuser))
)
H = [
self.standard_html_header(REQUEST),
"""<div style="margin: 1em;">
<h2>Scolarité du département %s</h2>"""
% deptfoldername,
"""<p>Ce site est
<font color="#FF0000"><b>réservé au personnel du département</b></font>.
</p>""",
self.authentication_form(destination="Scolarite"),
"""
<p>Pour quitter, <a href="acl_users/logout">logout</a></p>
<p><a href="%s">Retour à l'accueil</a></p>
</div>
"""
% self.ScoDocURL(),
self.standard_html_footer(REQUEST),
]
return "\n".join(H)
security.declareProtected("View", "doLogin")
def doLogin(self, REQUEST=None, destination=None):
"redirect to destination after login"
if destination:
return REQUEST.RESPONSE.redirect(destination)
security.declareProtected("View", "docLogin")
docLogin = DTMLFile("dtml/docLogin", globals())
security.declareProtected("View", "docLogout")
docLogout = DTMLFile("dtml/docLogout", globals())
security.declareProtected("View", "query_string_to_form_inputs")
def query_string_to_form_inputs(self, query_string=""):
"""Return html snippet representing the query string as POST form hidden inputs.
This is useful in conjonction with exUserfolder to correctly redirect the response
after authentication.
"""
H = []
for a in query_string.split("&"):
if a:
nv = a.split("=")
if len(nv) == 2:
name, value = nv
H.append(
'<input type="hidden" name="'
+ name
+ '" value="'
+ value
+ '"/>'
)
return "<!-- query string -->\n" + "\n".join(H)
security.declareProtected("View", "standard_html_header")
def standard_html_header(self, REQUEST=None):
"""Standard HTML header for pages outside depts"""
# not used in ZScolar, see sco_header
return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>ScoDoc: accueil</title>
<META http-equiv="Content-Type" content="text/html; charset=%s">
<META http-equiv="Content-Style-Type" content="text/css">
<META name="LANG" content="fr">
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
<link HREF="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css"/>
</head><body>%s""" % (
SCO_ENCODING,
CUSTOM_HTML_HEADER_CNX,
)
security.declareProtected("View", "standard_html_footer")
def standard_html_footer(self, REQUEST=None):
return """<p class="footer">
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
<p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:%s">%s</a></p>
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
</body></html>""" % (
SCO_USERS_LIST,
SCO_USERS_LIST,
)
# sendEmail is not used through the web
def sendEmail(self, msg):
# sends an email to the address using the mailhost, if there is one
try:
mail_host = self.MailHost
except:
log("warning: sendEmail: no MailHost found !")
return
# a failed notification shouldn't cause a Zope error on a site.
try:
mail_host.send(msg.as_string())
log("sendEmail: ok")
except Exception as e:
log("sendEmail: exception while sending message")
log(e)
pass
def sendEmailFromException(self, msg):
# Send email by hand, as it seems to be not possible to use Zope Mail Host
# from an exception handler (see https://bugs.launchpad.net/zope2/+bug/246748)
log("sendEmailFromException")
try:
p = os.popen("sendmail -t", "w") # old brute force method
p.write(msg.as_string())
exitcode = p.close()
if exitcode:
log("sendmail exit code: %s" % exitcode)
except:
log("an exception occurred sending mail")
security.declareProtected("View", "standard_error_message")
def standard_error_message(
self,
error_value=None,
error_message=None,
error_type=None,
error_traceback=None,
error_tb=None,
**kv
):
"Recuperation des exceptions Zope"
sco_exc_mail = SCO_EXC_MAIL
sco_dev_mail = SCO_DEV_MAIL
# neat (or should I say dirty ?) hack to get REQUEST
# in fact, our caller (probably SimpleItem.py) has the REQUEST variable
# that we'd like to use for our logs, but does not pass it as an argument.
try:
frame = inspect.currentframe()
REQUEST = frame.f_back.f_locals["REQUEST"]
except:
REQUEST = {}
# Authentication uses exceptions, pass them up
HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "")
if error_type == "LoginRequired":
# raise 'LoginRequired', '' # copied from exuserFolder (beurk, old style exception...)
# if REQUEST:
# REQUEST.response.setStatus( 401, "Unauthorized") # ??????
log("LoginRequired from %s" % HTTP_X_FORWARDED_FOR)
self.login_page = error_value
return error_value
elif error_type == "Unauthorized":
log("Unauthorized from %s" % HTTP_X_FORWARDED_FOR)
return self.acl_users.docLogin(self, REQUEST=REQUEST)
log("exception caught: %s" % error_type)
log(traceback.format_exc())
if error_type == "ScoGenError":
return "<p>" + str(error_value) + "</p>"
elif error_type in ("ScoValueError", "FormatError"):
# Not a bug, presents a gentle message to the user:
H = [
self.standard_html_header(REQUEST),
"""<h2>Erreur !</h2><p>%s</p>""" % error_value,
]
if error_value.dest_url:
H.append('<p><a href="%s">Continuer</a></p>' % error_value.dest_url)
H.append(self.standard_html_footer(REQUEST))
return "\n".join(H)
else: # Other exceptions, try carefully to build an error page...
# log('exc A')
H = []
try:
H.append(self.standard_html_header(REQUEST))
except:
pass
H.append(
"""<table border="0" width="100%%"><tr valign="top">
<td width="10%%" align="center"></td>
<td width="90%%"><h2>Erreur !</h2>
<p>Une erreur est survenue</p>
<p>
<strong>Error Type: %(error_type)s</strong><br>
<strong>Error Value: %(error_value)s</strong><br>
</p>
<hr noshade>
<p>L'URL est peut-etre incorrecte ?</p>
<p>Si l'erreur persiste, contactez Emmanuel Viennet:
<a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>
en copiant ce message d'erreur et le contenu du cadre bleu ci-dessous si possible.
</p>
</td></tr>
</table> """
% vars()
)
# display error traceback (? may open a security risk via xss attack ?)
# log('exc B')
txt_html = self._report_request(REQUEST, fmt="html")
H.append(
"""<h4 class="scodoc">Zope Traceback (à envoyer par mail à <a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>)</h4><div style="background-color: rgb(153,153,204); border: 1px;">
%(error_tb)s
<p><b>Informations:</b><br/>
%(txt_html)s
</p>
</div>
<p>Merci de votre patience !</p>
"""
% vars()
)
try:
H.append(self.standard_html_footer(REQUEST))
except:
log("no footer found for error page")
pass
# --- Mail:
error_traceback_txt = scodoc_html2txt(error_tb)
txt = (
"""
ErrorType: %(error_type)s
%(error_traceback_txt)s
"""
% vars()
)
self.send_debug_alert(txt, REQUEST=REQUEST)
# ---
log("done processing exception")
# log( '\n page=\n' + '\n'.join(H) )
return "\n".join(H)
def _report_request(self, REQUEST, fmt="txt"):
"""string describing current request for bug reports"""
AUTHENTICATED_USER = REQUEST.get("AUTHENTICATED_USER", "")
dt = time.asctime()
URL = REQUEST.get("URL", "")
QUERY_STRING = REQUEST.get("QUERY_STRING", "")
if QUERY_STRING:
QUERY_STRING = "?" + QUERY_STRING
METHOD = REQUEST.get("REQUEST_METHOD", "")
if fmt == "txt":
REFERER = REQUEST.get("HTTP_REFERER", "")
HTTP_USER_AGENT = REQUEST.get("HTTP_USER_AGENT", "")
else:
REFERER = "na"
HTTP_USER_AGENT = "na"
form = REQUEST.get("form", "")
HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "")
svn_version = get_svn_version(self.file_path)
SCOVERSION = VERSION.SCOVERSION
txt = (
"""
Version: %(SCOVERSION)s
User: %(AUTHENTICATED_USER)s
Date: %(dt)s
URL: %(URL)s%(QUERY_STRING)s
Method: %(METHOD)s
REFERER: %(REFERER)s
Form: %(form)s
Origin: %(HTTP_X_FORWARDED_FOR)s
Agent: %(HTTP_USER_AGENT)s
subversion: %(svn_version)s
"""
% vars()
)
if fmt == "html":
txt = txt.replace("\n", "<br/>")
return txt
security.declareProtected(
ScoSuperAdmin, "send_debug_alert"
) # not called through the web
def send_debug_alert(self, txt, REQUEST=None):
"""Send an alert email (bug report) to ScoDoc developpers"""
if not SCO_EXC_MAIL:
log("send_debug_alert: email disabled")
return
if REQUEST:
txt = self._report_request(REQUEST) + txt
URL = REQUEST.get("URL", "")
else:
URL = "send_debug_alert"
msg = MIMEMultipart()
subj = Header("[scodoc] exc %s" % URL, SCO_ENCODING)
msg["Subject"] = subj
recipients = [SCO_EXC_MAIL]
msg["To"] = " ,".join(recipients)
msg["From"] = "scodoc-alert"
msg.epilogue = ""
msg.attach(MIMEText(txt, "plain", SCO_ENCODING))
self.sendEmailFromException(msg)
log("Sent mail alert:\n" + txt)
security.declareProtected("View", "scodoc_admin")
def scodoc_admin(self, REQUEST=None):
"""Page Operations d'administration
"""
e = self._check_admin_perm(REQUEST)
if e:
return e
H = [
self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"),
"""
<h3>Administration ScoDoc</h3>
<p><a href="change_admin_user_form">changer le mot de passe super-administrateur</a></p>
<p><a href="%s">retour à la page d'accueil</a></p>
<h4 class="scodoc">Création d'un département</h4>
<p class="help_important">Le département doit avoir été créé au préalable sur le serveur en utilisant le script
<tt>create_dept.sh</tt> (à lancer comme <tt>root</tt> dans le répertoire <tt>config</tt> de ScoDoc).
</p>"""
% self.absolute_url(),
]
deptList = [x.id for x in self.list_depts()] # definis dans Zope
deptIds = Set(self._list_depts_ids()) # definis sur le filesystem
existingDepts = Set(deptList)
addableDepts = deptIds - existingDepts
if not addableDepts:
# aucun departement defini: aide utilisateur
H.append("<p>Aucun département à ajouter !</p>")
else:
H.append("""<form action="create_dept"><select name="DeptId"/>""")
for deptId in addableDepts:
H.append("""<option value="%s">%s</option>""" % (deptId, deptId))
H.append(
"""</select>
<input type="submit" value="Créer département">
</form>"""
)
if deptList:
H.append(
"""
<h4 class="scodoc">Suppression d'un département</h4>
<p>Ceci permet de supprimer le site web associé à un département, mais n'affecte pas la base de données
(le site peut donc être recréé sans perte de données).
</p>
<form action="delete_dept">
<select name="DeptId">
"""
)
for deptFolder in self.list_depts():
H.append(
'<option value="%s">%s</option>' % (deptFolder.id, deptFolder.id)
)
H.append(
"""</select>
<input type="submit" value="Supprimer département">
</form>"""
)
H.append("""</body></html>""")
return "\n".join(H)
def _list_depts_ids(self):
"""Liste de id de departements definis par create_dept.sh
(fichiers depts/*.cfg)
"""
filenames = glob.glob(SCODOC_VAR_DIR + "/config/depts/*.cfg")
ids = [os.path.split(os.path.splitext(f)[0])[1] for f in filenames]
return ids
security.declareProtected("View", "http_expiration_date")
def http_expiration_date(self):
"http expiration date for cachable elements (css, ...)"
d = datetime.timedelta(minutes=10)
return (datetime.datetime.utcnow() + d).strftime("%a, %d %b %Y %H:%M:%S GMT")
security.declareProtected("View", "get_etud_dept")
def get_etud_dept(self, REQUEST=None):
"""Returns the dept id (eg "GEII") of an etud (identified by etudid, INE or NIP in REQUEST).
Warning: This function is inefficient and its result should be cached.
"""
depts = self.list_depts()
depts_etud = [] # liste des depts où l'etud est defini
for dept in depts:
etuds = dept.Scolarite.getEtudInfo(REQUEST=REQUEST)
if etuds:
depts_etud.append((dept, etuds))
if not depts_etud:
return "" # not found
elif len(depts_etud) == 1:
return depts_etud[0][0].id
# inscriptions dans plusieurs departements: cherche la plus recente
last_dept = None
last_date = None
for (dept, etuds) in depts_etud:
dept.Scolarite.fillEtudsInfo(etuds)
etud = etuds[0]
if etud["sems"]:
if (not last_date) or (etud["sems"][0]["date_fin_iso"] > last_date):
last_date = etud["sems"][0]["date_fin_iso"]
last_dept = dept
if not last_dept:
# est present dans plusieurs semestres mais inscrit dans aucun
return depts_etud[0][0]
return last_dept.id
security.declareProtected("View", "table_etud_in_accessible_depts")
table_etud_in_accessible_depts = sco_find_etud.table_etud_in_accessible_depts
security.declareProtected("View", "search_inscr_etud_by_nip")
search_inscr_etud_by_nip = sco_find_etud.search_inscr_etud_by_nip
def manage_addZScoDoc(self, id="ScoDoc", title="Site ScoDoc", REQUEST=None):
"Add a ZScoDoc instance to a folder."
log("============== creating a new ScoDoc instance =============")
zscodoc = ZScoDoc(
id, title
) # ne cree (presque rien), tout se passe lors du 1er accès
self._setObject(id, zscodoc)
if REQUEST is not None:
REQUEST.RESPONSE.redirect("%s/manage_workspace" % REQUEST.URL1)
return id

1334
ZScoUsers.py Normal file

File diff suppressed because it is too large Load Diff

2830
ZScolar.py Normal file

File diff suppressed because it is too large Load Diff

5
ZopeProducts/README Normal file
View File

@ -0,0 +1,5 @@
Produits Zope2 anciens et adaptes pour ScoDoc
E. Viennet 2013

View File

@ -0,0 +1,372 @@
# ZPsycopgDA/DA.py - ZPsycopgDA Zope product: Database Connection
#
# Copyright (C) 2004-2010 Federico Di Gregorio <fog@debian.org>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# psycopg2 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 Lesser General Public
# License for more details.
# Import modules needed by _psycopg to allow tools like py2exe to do
# their work without bothering about the module dependencies.
import sys
import time
import db
import re
import Acquisition
import Shared.DC.ZRDB.Connection
from db import DB
from Globals import HTMLFile
from ExtensionClass import Base
from App.Dialogs import MessageDialog
from DateTime import DateTime
# ImageFile is deprecated in Zope >= 2.9
try:
from App.ImageFile import ImageFile
except ImportError:
# Zope < 2.9. If PIL's installed with a .pth file, we're probably
# hosed.
from ImageFile import ImageFile
# import psycopg and functions/singletons needed for date/time conversions
import psycopg2
from psycopg2 import NUMBER, STRING, ROWID, DATETIME
from psycopg2.extensions import INTEGER, LONGINTEGER, FLOAT, BOOLEAN, DATE
from psycopg2.extensions import TIME, INTERVAL
from psycopg2.extensions import new_type, register_type
# add a new connection to a folder
manage_addZPsycopgConnectionForm = HTMLFile('dtml/add',globals())
def manage_addZPsycopgConnection(self, id, title, connection_string,
zdatetime=None, tilevel=2,
encoding='', check=None, REQUEST=None):
"""Add a DB connection to a folder."""
self._setObject(id, Connection(id, title, connection_string,
zdatetime, check, tilevel, encoding))
if REQUEST is not None: return self.manage_main(self, REQUEST)
# the connection object
class Connection(Shared.DC.ZRDB.Connection.Connection):
"""ZPsycopg Connection."""
_isAnSQLConnection = 1
id = 'Psycopg2_database_connection'
database_type = 'Psycopg2'
meta_type = title = 'Z Psycopg 2 Database Connection'
icon = 'misc_/conn'
def __init__(self, id, title, connection_string,
zdatetime, check=None, tilevel=2, encoding='UTF-8'):
self.zdatetime = zdatetime
self.id = str(id)
self.edit(title, connection_string, zdatetime,
check=check, tilevel=tilevel, encoding=encoding)
def factory(self):
return DB
## connection parameters editing ##
def edit(self, title, connection_string,
zdatetime, check=None, tilevel=2, encoding='UTF-8'):
self.title = title
self.connection_string = connection_string
self.zdatetime = zdatetime
self.tilevel = tilevel
self.encoding = encoding
if check: self.connect(self.connection_string)
manage_properties = HTMLFile('dtml/edit', globals())
def manage_edit(self, title, connection_string,
zdatetime=None, check=None, tilevel=2, encoding='UTF-8',
REQUEST=None):
"""Edit the DB connection."""
self.edit(title, connection_string, zdatetime,
check=check, tilevel=tilevel, encoding=encoding)
if REQUEST is not None:
msg = "Connection edited."
return self.manage_main(self,REQUEST,manage_tabs_message=msg)
def connect(self, s):
try:
self._v_database_connection.close()
except:
pass
# check psycopg version and raise exception if does not match
check_psycopg_version(psycopg2.__version__)
self._v_connected = ''
dbf = self.factory()
# TODO: let the psycopg exception propagate, or not?
self._v_database_connection = dbf(
self.connection_string, self.tilevel, self.get_type_casts(), self.encoding)
self._v_database_connection.open()
self._v_connected = DateTime()
return self
def get_type_casts(self):
# note that in both cases order *is* important
if self.zdatetime:
return ZDATETIME, ZDATE, ZTIME
else:
return DATETIME, DATE, TIME
## browsing and table/column management ##
manage_options = Shared.DC.ZRDB.Connection.Connection.manage_options
# + (
# {'label': 'Browse', 'action':'manage_browse'},)
#manage_tables = HTMLFile('dtml/tables', globals())
#manage_browse = HTMLFile('dtml/browse', globals())
info = None
def table_info(self):
return self._v_database_connection.table_info()
def __getitem__(self, name):
if name == 'tableNamed':
if not hasattr(self, '_v_tables'): self.tpValues()
return self._v_tables.__of__(self)
raise KeyError, name
def tpValues(self):
res = []
conn = self._v_database_connection
for d in conn.tables(rdb=0):
try:
name = d['TABLE_NAME']
b = TableBrowser()
b.__name__ = name
b._d = d
b._c = c
try:
b.icon = table_icons[d['TABLE_TYPE']]
except:
pass
r.append(b)
except:
pass
return res
def check_psycopg_version(version):
"""
Check that the psycopg version used is compatible with the zope adpter.
"""
try:
m = re.match(r'\d+\.\d+(\.\d+)?', version.split(' ')[0])
tver = tuple(map(int, m.group().split('.')))
except:
raise ImportError("failed to parse psycopg version %s" % version)
if tver < (2, 4):
raise ImportError("psycopg version %s is too old" % version)
if tver in ((2,4,2), (2,4,3)):
raise ImportError("psycopg version %s is known to be buggy" % version)
## database connection registration data ##
classes = (Connection,)
meta_types = ({'name':'Z Psycopg 2 Database Connection',
'action':'manage_addZPsycopgConnectionForm'},)
folder_methods = {
'manage_addZPsycopgConnection': manage_addZPsycopgConnection,
'manage_addZPsycopgConnectionForm': manage_addZPsycopgConnectionForm}
__ac_permissions__ = (
('Add Z Psycopg Database Connections',
('manage_addZPsycopgConnectionForm', 'manage_addZPsycopgConnection')),)
# add icons
misc_={'conn': ImageFile('icons/DBAdapterFolder_icon.gif', globals())}
for icon in ('table', 'view', 'stable', 'what', 'field', 'text', 'bin',
'int', 'float', 'date', 'time', 'datetime'):
misc_[icon] = ImageFile('icons/%s.gif' % icon, globals())
## zope-specific psycopg typecasters ##
# convert an ISO timestamp string from postgres to a Zope DateTime object
def _cast_DateTime(iso, curs):
if iso:
if iso in ['-infinity', 'infinity']:
return iso
else:
return DateTime(iso)
# convert an ISO date string from postgres to a Zope DateTime object
def _cast_Date(iso, curs):
if iso:
if iso in ['-infinity', 'infinity']:
return iso
else:
return DateTime(iso)
# Convert a time string from postgres to a Zope DateTime object.
# NOTE: we set the day as today before feeding to DateTime so
# that it has the same DST settings.
def _cast_Time(iso, curs):
if iso:
if iso in ['-infinity', 'infinity']:
return iso
else:
return DateTime(time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(time.time())[:3]+
time.strptime(iso[:8], "%H:%M:%S")[3:]))
# NOTE: we don't cast intervals anymore because they are passed
# untouched to Zope.
def _cast_Interval(iso, curs):
return iso
ZDATETIME = new_type((1184, 1114), "ZDATETIME", _cast_DateTime)
ZINTERVAL = new_type((1186,), "ZINTERVAL", _cast_Interval)
ZDATE = new_type((1082,), "ZDATE", _cast_Date)
ZTIME = new_type((1083,), "ZTIME", _cast_Time)
## table browsing helpers ##
class TableBrowserCollection(Acquisition.Implicit):
pass
class Browser(Base):
def __getattr__(self, name):
try:
return self._d[name]
except KeyError:
raise AttributeError, name
class values:
def len(self):
return 1
def __getitem__(self, i):
try:
return self._d[i]
except AttributeError:
pass
self._d = self._f()
return self._d[i]
class TableBrowser(Browser, Acquisition.Implicit):
icon = 'what'
Description = check = ''
info = HTMLFile('table_info', globals())
menu = HTMLFile('table_menu', globals())
def tpValues(self):
v = values()
v._f = self.tpValues_
return v
def tpValues_(self):
r=[]
tname=self.__name__
for d in self._c.columns(tname):
b=ColumnBrowser()
b._d=d
try: b.icon=field_icons[d['Type']]
except: pass
b.TABLE_NAME=tname
r.append(b)
return r
def tpId(self): return self._d['TABLE_NAME']
def tpURL(self): return "Table/%s" % self._d['TABLE_NAME']
def Name(self): return self._d['TABLE_NAME']
def Type(self): return self._d['TABLE_TYPE']
manage_designInput=HTMLFile('designInput',globals())
def manage_buildInput(self, id, source, default, REQUEST=None):
"Create a database method for an input form"
args=[]
values=[]
names=[]
columns=self._columns
for i in range(len(source)):
s=source[i]
if s=='Null': continue
c=columns[i]
d=default[i]
t=c['Type']
n=c['Name']
names.append(n)
if s=='Argument':
values.append("<dtml-sqlvar %s type=%s>'" %
(n, vartype(t)))
a='%s%s' % (n, boboType(t))
if d: a="%s=%s" % (a,d)
args.append(a)
elif s=='Property':
values.append("<dtml-sqlvar %s type=%s>'" %
(n, vartype(t)))
else:
if isStringType(t):
if find(d,"\'") >= 0: d=join(split(d,"\'"),"''")
values.append("'%s'" % d)
elif d:
values.append(str(d))
else:
raise ValueError, (
'no default was given for <em>%s</em>' % n)
class ColumnBrowser(Browser):
icon='field'
def check(self):
return ('\t<input type=checkbox name="%s.%s">' %
(self.TABLE_NAME, self._d['Name']))
def tpId(self): return self._d['Name']
def tpURL(self): return "Column/%s" % self._d['Name']
def Description(self):
d=self._d
if d['Scale']:
return " %(Type)s(%(Precision)s,%(Scale)s) %(Nullable)s" % d
else:
return " %(Type)s(%(Precision)s) %(Nullable)s" % d
table_icons={
'TABLE': 'table',
'VIEW':'view',
'SYSTEM_TABLE': 'stable',
}
field_icons={
NUMBER.name: 'i',
STRING.name: 'text',
DATETIME.name: 'date',
INTEGER.name: 'int',
FLOAT.name: 'float',
BOOLEAN.name: 'bin',
ROWID.name: 'int'
}

View File

@ -0,0 +1,29 @@
# ZPsycopgDA/__init__.py - ZPsycopgDA Zope product
#
# Copyright (C) 2004-2010 Federico Di Gregorio <fog@debian.org>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# psycopg2 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 Lesser General Public
# License for more details.
# Import modules needed by _psycopg to allow tools like py2exe to do
# their work without bothering about the module dependencies.
__doc__ = "ZPsycopg Database Adapter Registration."
__version__ = '2.4.6'
import DA
def initialize(context):
context.registerClass(
DA.Connection,
permission = 'Add Z Psycopg 2 Database Connections',
constructors = (DA.manage_addZPsycopgConnectionForm,
DA.manage_addZPsycopgConnection),
icon = 'icons/DBAdapterFolder_icon.gif')

View File

@ -0,0 +1,209 @@
# ZPsycopgDA/db.py - query execution
#
# Copyright (C) 2004-2010 Federico Di Gregorio <fog@debian.org>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# psycopg2 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 Lesser General Public
# License for more details.
# Import modules needed by _psycopg to allow tools like py2exe to do
# their work without bothering about the module dependencies.
from Shared.DC.ZRDB.TM import TM
from Shared.DC.ZRDB import dbi_db
from ZODB.POSException import ConflictError
import site
import pool
import psycopg2
from psycopg2.extensions import INTEGER, LONGINTEGER, FLOAT, BOOLEAN, DATE, TIME
from psycopg2.extensions import TransactionRollbackError, register_type
from psycopg2 import NUMBER, STRING, ROWID, DATETIME
# the DB object, managing all the real query work
class DB(TM, dbi_db.DB):
_p_oid = _p_changed = _registered = None
def __init__(self, dsn, tilevel, typecasts, enc='utf-8'):
self.dsn = dsn
self.tilevel = tilevel
self.typecasts = typecasts
if enc is None or enc == "":
self.encoding = "utf-8"
else:
self.encoding = enc
self.failures = 0
self.calls = 0
self.make_mappings()
def getconn(self, init=True):
# if init is False we are trying to get hold on an already existing
# connection, so we avoid to (re)initialize it risking errors.
conn = pool.getconn(self.dsn)
if init:
# use set_session where available as in these versions
# set_isolation_level generates an extra query.
if psycopg2.__version__ >= '2.4.2':
conn.set_session(isolation_level=int(self.tilevel))
else:
conn.set_isolation_level(int(self.tilevel))
conn.set_client_encoding(self.encoding)
for tc in self.typecasts:
register_type(tc, conn)
return conn
def putconn(self, close=False):
try:
conn = pool.getconn(self.dsn, False)
except AttributeError:
pass
pool.putconn(self.dsn, conn, close)
def getcursor(self):
conn = self.getconn(False)
return conn.cursor()
def _finish(self, *ignored):
try:
conn = self.getconn(False)
conn.commit()
self.putconn()
except AttributeError:
pass
def _abort(self, *ignored):
try:
conn = self.getconn(False)
conn.rollback()
self.putconn()
except AttributeError:
pass
def open(self):
# this will create a new pool for our DSN if not already existing,
# then get and immediately release a connection
self.getconn()
self.putconn()
def close(self):
# FIXME: if this connection is closed we flush all the pool associated
# with the current DSN; does this makes sense?
pool.flushpool(self.dsn)
def sortKey(self):
return 1
def make_mappings(self):
"""Generate the mappings used later by self.convert_description()."""
self.type_mappings = {}
for t, s in [(INTEGER,'i'), (LONGINTEGER, 'i'), (NUMBER, 'n'),
(BOOLEAN,'n'), (ROWID, 'i'),
(DATETIME, 'd'), (DATE, 'd'), (TIME, 'd')]:
for v in t.values:
self.type_mappings[v] = (t, s)
def convert_description(self, desc, use_psycopg_types=False):
"""Convert DBAPI-2.0 description field to Zope format."""
items = []
for name, typ, width, ds, p, scale, null_ok in desc:
m = self.type_mappings.get(typ, (STRING, 's'))
items.append({
'name': name,
'type': use_psycopg_types and m[0] or m[1],
'width': width,
'precision': p,
'scale': scale,
'null': null_ok,
})
return items
## tables and rows ##
def tables(self, rdb=0, _care=('TABLE', 'VIEW')):
self._register()
c = self.getcursor()
c.execute(
"SELECT t.tablename AS NAME, 'TABLE' AS TYPE "
" FROM pg_tables t WHERE tableowner <> 'postgres' "
"UNION SELECT v.viewname AS NAME, 'VIEW' AS TYPE "
" FROM pg_views v WHERE viewowner <> 'postgres' "
"UNION SELECT t.tablename AS NAME, 'SYSTEM_TABLE\' AS TYPE "
" FROM pg_tables t WHERE tableowner = 'postgres' "
"UNION SELECT v.viewname AS NAME, 'SYSTEM_TABLE' AS TYPE "
"FROM pg_views v WHERE viewowner = 'postgres'")
res = []
for name, typ in c.fetchall():
if typ in _care:
res.append({'TABLE_NAME': name, 'TABLE_TYPE': typ})
self.putconn()
return res
def columns(self, table_name):
self._register()
c = self.getcursor()
try:
r = c.execute('SELECT * FROM "%s" WHERE 1=0' % table_name)
except:
return ()
self.putconn()
return self.convert_description(c.description, True)
## query execution ##
def query(self, query_string, max_rows=None, query_data=None):
self._register()
self.calls = self.calls+1
desc = ()
res = []
nselects = 0
c = self.getcursor()
try:
for qs in [x for x in query_string.split('\0') if x]:
try:
if query_data:
c.execute(qs, query_data)
else:
c.execute(qs)
except TransactionRollbackError:
# Ha, here we have to look like we are the ZODB raising conflict errrors, raising ZPublisher.Publish.Retry just doesn't work
#logging.debug("Serialization Error, retrying transaction", exc_info=True)
raise ConflictError("TransactionRollbackError from psycopg2")
except psycopg2.OperationalError:
#logging.exception("Operational error on connection, closing it.")
try:
# Only close our connection
self.putconn(True)
except:
#logging.debug("Something went wrong when we tried to close the pool", exc_info=True)
pass
if c.description is not None:
nselects += 1
if c.description != desc and nselects > 1:
raise psycopg2.ProgrammingError(
'multiple selects in single query not allowed')
if max_rows:
res = c.fetchmany(max_rows)
else:
res = c.fetchall()
desc = c.description
self.failures = 0
except StandardError, err:
self._abort()
raise err
return self.convert_description(desc), res

View File

@ -0,0 +1,108 @@
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Z Psycopg 2 Database Connection',
help_product='ZPsycopgDA',
help_topic='ZPsycopgDA-Method-Add.stx'
)">
<p class="form-help">
A Zope Psycopg 2 Database Connection is used to connect and execute
queries on a PostgreSQL database.
</p>
<p class="form-help">
In the form below <em>Connection String</em> (also called the Data Source Name
or DSN for short) is a string... (TODO: finish docs)
</p>
<form action="manage_addZPsycopgConnection" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40"
value="Psycopg2_database_connection" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="Z Psycopg 2 Database Connection"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Connection string
</div>
</td>
<td align="left" valign="top">
<input type="text" name="connection_string" size="40" value="" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Connect immediately
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="check" value="YES" checked="YES" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Use Zope's internal DateTime
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="zdatetime" value="YES" checked="YES" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Transaction isolation level
</div>
</td>
<td align="left" valign="top">
<select name="tilevel:int">
<option value="4">Read uncommitted</option>
<option value="1">Read committed</option>
<option value="2" selected="YES">Repeatable read</option>
<option value="3">Serializable</option>
</select>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Encoding
</div>
</td>
<td align="left" valign="top">
<input type="text" name="encoding" size="40" value="" />
</td>
</tr>
<tr>
<td align="left" valign="top" colspan="2">
<div class="form-element">
<input class="form-element" type="submit" name="submit" value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>

View File

@ -0,0 +1,11 @@
<html>
<head><title><dtml-var title_or_id >tables</title></head>
<body bgcolor="#FFFFFF" link="#000099" vlink="#555555" alink="#77003B">
<dtml-var manage_tabs>
<dtml-tree header="info">
<IMG SRC="<dtml-var SCRIPT_NAME >/misc_/ZPsycopgDA/<dtml-var icon>"
ALT="<dtml-var Type>" BORDER="0">
<dtml-var Name><dtml-var Description>
</dtml-tree>
</body>
</html>

View File

@ -0,0 +1,84 @@
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<form action="manage_edit" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40"
value="&dtml-title;"/>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Connection string
</div>
</td>
<td align="left" valign="top">
<input type="text" name="connection_string" size="40"
value="&dtml-connection_string;" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Use Zope's internal DateTime
</div>
</td>
<td align="left" valign="top">
<input type="checkbox" name="zdatetime" value="YES"
<dtml-if expr="zdatetime">checked="YES"</dtml-if> />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Transaction isolation level
</div>
</td>
<td align="left" valign="top">
<select name="tilevel:int">
<option value="4"
<dtml-if expr="tilevel==4">selected="YES"</dtml-if>>
Read uncommitted</option>
<option value="1"
<dtml-if expr="tilevel==1">selected="YES"</dtml-if>>
Read committed</option>
<option value="2"
<dtml-if expr="tilevel==2">selected="YES"</dtml-if>>
Repeatable read</option>
<option value="3"
<dtml-if expr="tilevel==3">selected="YES"</dtml-if>>
Serializable</option>
</select>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Encoding
</div>
</td>
<td align="left" valign="top">
<input type="text" name="encoding" size="40"
value="&dtml-encoding;" />
</td>
</tr>
<tr>
<td align="left" valign="top" colspan="2">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Save Changes " />
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>

View File

@ -0,0 +1,7 @@
<dtml-var standard_html_header>
<dtml-var TABLE_TYPE><dtml-if TABLE_OWNER>
owned by <dtml-var TABLE_OWNER></dtml-if>
<dtml-if REMARKS><br><dtml-var REMARKS></dtml-if>
<dtml-var standard_html_footer>

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@ -0,0 +1,193 @@
# ZPsycopgDA/pool.py - ZPsycopgDA Zope product: connection pooling
#
# Copyright (C) 2004-2010 Federico Di Gregorio <fog@debian.org>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# psycopg2 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 Lesser General Public
# License for more details.
# Import modules needed by _psycopg to allow tools like py2exe to do
# their work without bothering about the module dependencies.
# All the connections are held in a pool of pools, directly accessible by the
# ZPsycopgDA code in db.py.
import threading
import psycopg2
from psycopg2.pool import PoolError
class AbstractConnectionPool(object):
"""Generic key-based pooling code."""
def __init__(self, minconn, maxconn, *args, **kwargs):
"""Initialize the connection pool.
New 'minconn' connections are created immediately calling 'connfunc'
with given parameters. The connection pool will support a maximum of
about 'maxconn' connections.
"""
self.minconn = minconn
self.maxconn = maxconn
self.closed = False
self._args = args
self._kwargs = kwargs
self._pool = []
self._used = {}
self._rused = {} # id(conn) -> key map
self._keys = 0
for i in range(self.minconn):
self._connect()
def _connect(self, key=None):
"""Create a new connection and assign it to 'key' if not None."""
conn = psycopg2.connect(*self._args, **self._kwargs)
if key is not None:
self._used[key] = conn
self._rused[id(conn)] = key
else:
self._pool.append(conn)
return conn
def _getkey(self):
"""Return a new unique key."""
self._keys += 1
return self._keys
def _getconn(self, key=None):
"""Get a free connection and assign it to 'key' if not None."""
if self.closed: raise PoolError("connection pool is closed")
if key is None: key = self._getkey()
if key in self._used:
return self._used[key]
if self._pool:
self._used[key] = conn = self._pool.pop()
self._rused[id(conn)] = key
return conn
else:
if len(self._used) == self.maxconn:
raise PoolError("connection pool exausted")
return self._connect(key)
def _putconn(self, conn, key=None, close=False):
"""Put away a connection."""
if self.closed: raise PoolError("connection pool is closed")
if key is None: key = self._rused[id(conn)]
if not key:
raise PoolError("trying to put unkeyed connection")
if len(self._pool) < self.minconn and not close:
self._pool.append(conn)
else:
conn.close()
# here we check for the presence of key because it can happen that a
# thread tries to put back a connection after a call to close
if not self.closed or key in self._used:
del self._used[key]
del self._rused[id(conn)]
def _closeall(self):
"""Close all connections.
Note that this can lead to some code fail badly when trying to use
an already closed connection. If you call .closeall() make sure
your code can deal with it.
"""
if self.closed: raise PoolError("connection pool is closed")
for conn in self._pool + list(self._used.values()):
try:
conn.close()
except:
pass
self.closed = True
class PersistentConnectionPool(AbstractConnectionPool):
"""A pool that assigns persistent connections to different threads.
Note that this connection pool generates by itself the required keys
using the current thread id. This means that until a thread puts away
a connection it will always get the same connection object by successive
`!getconn()` calls. This also means that a thread can't use more than one
single connection from the pool.
"""
def __init__(self, minconn, maxconn, *args, **kwargs):
"""Initialize the threading lock."""
import threading
AbstractConnectionPool.__init__(
self, minconn, maxconn, *args, **kwargs)
self._lock = threading.Lock()
# we we'll need the thread module, to determine thread ids, so we
# import it here and copy it in an instance variable
import thread
self.__thread = thread
def getconn(self):
"""Generate thread id and return a connection."""
key = self.__thread.get_ident()
self._lock.acquire()
try:
return self._getconn(key)
finally:
self._lock.release()
def putconn(self, conn=None, close=False):
"""Put away an unused connection."""
key = self.__thread.get_ident()
self._lock.acquire()
try:
if not conn: conn = self._used[key]
self._putconn(conn, key, close)
finally:
self._lock.release()
def closeall(self):
"""Close all connections (even the one currently in use.)"""
self._lock.acquire()
try:
self._closeall()
finally:
self._lock.release()
_connections_pool = {}
_connections_lock = threading.Lock()
def getpool(dsn, create=True):
_connections_lock.acquire()
try:
if not _connections_pool.has_key(dsn) and create:
_connections_pool[dsn] = \
PersistentConnectionPool(4, 200, dsn)
finally:
_connections_lock.release()
return _connections_pool[dsn]
def flushpool(dsn):
_connections_lock.acquire()
try:
_connections_pool[dsn].closeall()
del _connections_pool[dsn]
finally:
_connections_lock.release()
def getconn(dsn, create=True):
return getpool(dsn, create=create).getconn()
def putconn(dsn, conn, close=False):
getpool(dsn).putconn(conn, close=close)

View File

@ -0,0 +1,47 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: __init__.py,v 1.1 2004/11/10 14:15:34 akm Exp $
#import etcAuthSource
#import httpsAuthSource
#import mysqlAuthSource
import pgAuthSource
#import pgAuthSourceAlt
#import radiusAuthSource
#import smbAuthSource
#import usAuthSource
#import zodbAuthSource
#import zodbBTreeAuthSource
#
# These have special requirements for external libraries
# that my not be present.
#
# try:
# import nisAuthSource
# except:
# pass
# try:
# import LDAPAuthSource
# except:
# pass

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:36 akm Exp $
import pgAuthSource

View File

@ -0,0 +1,40 @@
<dtml-var "DialogHeader(_.None,_,DialogTitle='Add Postgresql Authentication Source')">
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-in>
<input type="HIDDEN" name="doProp" value="1">
<TABLE CELLSPACING="2">
<tr><th><dtml-babel src="'en'">Database Connection</dtml-babel>:</th>
<td>
<select name="pgauth_connection">
<dtml-in "SQLConnectionIDs()">
<option value="<dtml-var sequence-item>">
<dtml-var sequence-key></option>
</dtml-in>
</select>
</td>
</tr>
<tr>
<th><dtml-babel src="'en'">Table Name</dtml-babel>:</th>
<td><input type="text" name="pgauth_table" value="passwd"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Username Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_usernameColumn" value="username"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Password Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_passwordColumn" value="password"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Roles Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_rolesColumn" value="roles"></td>
</tr>
<TR>
<TD></TD>
<TD><BR><INPUT TYPE="SUBMIT" VALUE="<dtml-babel src="'en'">Add</dtml-babel>"></TD>
</TR>
</TABLE>
</FORM>
<dtml-var DialogFooter>

View File

@ -0,0 +1,37 @@
<dtml-var "DialogHeader(_.None,_,DialogTitle='Postgresql Authentication Source',dialog_width='100%')">
<dtml-var manage_tabs>
<FORM ACTION="manage_editAuthSource" METHOD="POST">
<TABLE CELLSPACING="2">
<tr><th><dtml-babel src="'en'">Database Connection</dtml-babel>:</th>
<td>
<select name="pgauth_connection">
<dtml-in "SQLConnectionIDs()">
<option value="<dtml-var sequence-item>"<dtml-if "currentAuthSource.connection==_['sequence-item']"> SELECTED</dtml-if>>
<dtml-var sequence-key></option>
</dtml-in>
</select>
</td>
</tr>
<tr>
<th><dtml-babel src="'en'">Table Name</dtml-babel>:</th>
<td><input type="text" name="pgauth_table" value="<dtml-var "currentAuthSource.table">"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Username Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_usernameColumn" value="<dtml-var "currentAuthSource.usernameColumn">"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Password Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_passwordColumn" value="<dtml-var "currentAuthSource.passwordColumn">"></td>
</tr>
<tr>
<th><dtml-babel src="'en'">Roles Column</dtml-babel>:</th>
<td><input type="text" name="pgauth_rolesColumn" value="<dtml-var "currentAuthSource.rolesColumn">"></td>
</tr>
<TR>
<TD></TD>
<TD><BR><INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">Edit</dtml-babel> "></TD>
</TR>
</TABLE>
</FORM>
<dtml-var DialogFooter>

View File

@ -0,0 +1,333 @@
#
# Extensible User Folder
#
# Postgres Authentication Source for exUserFolder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: pgAuthSource.py,v 1.1 2004/11/10 14:15:36 akm Exp $
#
# This class only authenticates users, it stores no properties.
#
import string,Acquisition
from Globals import HTMLFile, MessageDialog, INSTANCE_HOME
from OFS.Folder import Folder
from Products.ZSQLMethods.SQL import SQL
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
try:
from crypt import crypt
except:
from Products.exUserFolder.fcrypt.fcrypt import crypt
# debug XXX
# def xLOG(msg):
# f = open('/tmp/debug.log','a')
# f.write(msg+'\n')
# f.close()
def manage_addpgAuthSource(self, REQUEST):
""" Add a Postgres Auth Source """
connection=REQUEST['pgauth_connection']
table=REQUEST['pgauth_table']
usernameColumn=REQUEST['pgauth_usernameColumn']
passwordColumn=REQUEST['pgauth_passwordColumn']
rolesColumn=REQUEST['pgauth_rolesColumn']
o = pgAuthSource(connection, table, usernameColumn, passwordColumn,
rolesColumn)
self._setObject('pgAuthSource', o, None, None, 0)
o=getattr(self,'pgAuthSource')
if hasattr(o, 'postInitialisation'):
o.postInitialisation(REQUEST)
self.currentAuthSource=o
return ''
manage_addpgAuthSourceForm=HTMLFile('manage_addpgAuthSourceForm', globals())
manage_editpgAuthSourceForm=HTMLFile('manage_editpgAuthSourceForm', globals())
class pgAuthSource(Folder):
""" Authenticate Users against a Postgres Database """
meta_type='Authentication Source'
title='Postgresql Authentication'
icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
manage_tabs=Acquisition.Acquired
manage_editForm=manage_editpgAuthSourceForm
#
# You can define this to go off and do the authentication instead of
# using the basic one inside the User Object
#
remoteAuthMethod=None
def __init__(self, connection, table, usernameColumn, passwordColumn,
rolesColumn):
self.id='pgAuthSource'
self.connection=connection
self.table=table
self.usernameColumn=usernameColumn
self.passwordColumn=passwordColumn
self.rolesColumn=rolesColumn
self.addSQLQueries()
def manage_editAuthSource(self, REQUEST):
""" Edit a Postgres Auth Source """
self.connection=REQUEST['pgauth_connection']
self.table=REQUEST['pgauth_table']
self.usernameColumn=REQUEST['pgauth_usernameColumn']
self.passwordColumn=REQUEST['pgauth_passwordColumn']
self.rolesColumn=REQUEST['pgauth_rolesColumn']
self.delSQLQueries()
self.addSQLQueries() # Re-add queries with new parameters
def createUser(self, username, password, roles):
""" Add A Username """
if type(roles) != type([]):
if roles:
roles=list(roles)
else:
roles=[]
rolestring=''
for role in roles:
rolestring=rolestring+role+','
rolestring=rolestring[:-1]
secret=self.cryptPassword(username, password)
self.sqlInsertUser(username=username,
password=secret,
roles=rolestring)
self._v_lastUser={}
def updateUser(self, username, password, roles):
if type(roles) != type([]):
if roles:
roles=list(roles)
else:
roles=[]
rolestring=''
for role in roles:
print role
rolestring=rolestring+role+','
rolestring=rolestring[:-1]
# Don't change passwords if it's null
if password:
secret=self.cryptPassword(username, password)
self.sqlUpdateUserPassword(username=username,
password=secret)
self.sqlUpdateUser(username=username,
roles=rolestring)
self._v_lastUser={}
def delSQLQueries(self):
sqllist=self.objectIds('Z SQL Method')
self.manage_delObjects(ids=sqllist)
def addSQLQueries(self):
sqlListUsers=SQL(
'sqlListUsers',
'List All Users',
self.connection,
'table=%s'%(self.table),
_sqlListUsers)
self._setObject('sqlListUsers', sqlListUsers)
sqlListOneUser=SQL(
'sqlListOneUser',
'List ONE User',
self.connection,
'table=%s usernameColumn=%s username:string'%(
self.table, self.usernameColumn),
_sqlListOneUser)
self._setObject('sqlListOneUser', sqlListOneUser)
sqlDeleteOneUser=SQL(
'sqlDeleteOneUser',
'Delete One User',
self.connection,
'table=%s usernameColumn=%s username:string'%(
self.table,self.usernameColumn),
_sqlDeleteOneUser)
self._setObject('sqlDeleteOneUser', sqlDeleteOneUser)
sqlInsertUser=SQL(
'sqlInsertUser',
'Insert One User',
self.connection,
'table=%s usernameColumn=%s passwordColumn=%s rolesColumn=%s username:string password:string roles:string'%(
self.table, self.usernameColumn, self.passwordColumn, self.rolesColumn),
_sqlInsertUser)
self._setObject('sqlInsertUser', sqlInsertUser)
sqlUpdateUser=SQL(
'sqlUpdateUser',
'Update User',
self.connection,
'table=%s rolesColumn=%s username:string roles:string'%(self.table, self.rolesColumn),
_sqlUpdateUser)
self._setObject('sqlUpdateUser', sqlUpdateUser)
sqlUpdateUserPassword=SQL(
'sqlUpdateUserPassword',
'Update just the password',
self.connection,
'table=%s usernameColumn=%s passwordColumn=%s username:string password:string'%(self.table, self.usernameColumn, self.passwordColumn),
_sqlUpdateUserPassword)
self._setObject('sqlUpdateUserPassword', sqlUpdateUserPassword)
def cryptPassword_old(self, username, password):
salt =username[:2]
secret = crypt(password, salt)
return secret
def deleteUsers(self, userids):
for uid in userids:
self.sqlDeleteOneUser(username=uid)
self._v_lastUser={}
def listUserNames(self):
"""Returns a real list of user names """
users = []
result=self.sqlListUsers()
for n in result:
username=sqlattr(n,self.usernameColumn)
users.append(username)
return users
def listUsers(self):
"""Returns a list of user names or [] if no users exist"""
users = []
result=self.sqlListUsers()
for n in result:
roles=[]
username=sqlattr(n,self.usernameColumn)
if sqlattr(n, self.rolesColumn):
roles=string.split(sqlattr(n,self.rolesColumn),',')
password=sqlattr(n, self.passwordColumn)
N={'username':username, 'password':password, 'roles':roles}
users.append(N)
return users
def listOneUser(self,username):
#xLOG('pg.listOneUser(%s)' % username)
if getattr(self, '_v_lastUser', {}):
if self._v_lastUser['username']==username:
return self._v_lastUser['users']
#xLOG('pg.listOneUser continuing')
users = []
result=self.sqlListOneUser(username=username)
#xLOG('pg.listOneUser result=%s' % result)
for n in result:
roles=[]
username=sqlattr(n,self.usernameColumn)
password=sqlattr(n,self.passwordColumn)
if sqlattr(n, self.rolesColumn):
roles=string.split(sqlattr(n,self.rolesColumn),',') #Andreas
N={'username':username, 'password':password, 'roles':roles}
users.append(N)
self._v_lastUser={}
self._v_lastUser['username']=username
self._v_lastUser['users']=users
return users
def postInitialisation(self, REQUEST):
self._v_lastUser={}
pgAuthReg=PluginRegister('pgAuthSource', 'Postgresql Authentication Source',
pgAuthSource, manage_addpgAuthSourceForm,
manage_addpgAuthSource,
manage_editpgAuthSourceForm)
exUserFolder.authSources['pgAuthSource']=pgAuthReg
from string import upper, lower
import Missing
mt=type(Missing.Value)
def typeconv(val):
if type(val)==mt:
return ''
return val
def sqlattr(ob, attr):
name=attr
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
attr=upper(attr)
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
attr=lower(attr)
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
raise NameError, name
_sqlListUsers="""
SELECT * FROM <dtml-var table>
"""
_sqlListOneUser="""
SELECT * FROM <dtml-var table>
where <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
"""
_sqlDeleteOneUser="""
DELETE FROM <dtml-var table>
where <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
"""
_sqlInsertUser="""
INSERT INTO <dtml-var table> (<dtml-var usernameColumn>, <dtml-var passwordColumn>, <dtml-var rolesColumn>)
VALUES (<dtml-sqlvar username type=string>,
<dtml-sqlvar password type=string>,
<dtml-sqlvar roles type=string>)
"""
_sqlUpdateUserPassword="""
UPDATE <dtml-var table> set <dtml-var passwordColumn>=<dtml-sqlvar password type=string>
WHERE <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
"""
_sqlUpdateUser="""
UPDATE <dtml-var table> set <dtml-var rolesColumn>=<dtml-sqlvar roles type=string>
WHERE <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
"""

View File

@ -0,0 +1,865 @@
Changes for 0.50.1
Add a README.Upgrading file to explain the impact of the 0.50.0 source
restructure, since people don't seem to be reading this file. --akm
Fix the default docLogin to use &dtml-URL as the default destination.
I porked the fcrypt import. It obviously doesn't get imported here since
I have a crypt module installed. -- akm
Fixed; https://sourceforge.net/tracker/?func=detail&aid=1084903&group_id=36318&atid=416446
thanks to vigine -- akm
Changes for 0.50.0
Restructured Source Tree. This will make this version incompatible with
previous versions, as the classes have moved. This breaks upgrading existing
installs unless you keep the old classes around. If you only use external
Auth/Prop/Group sources, you will probably be unaffected.
o Auth Sources moved to single directory
o Prop Sources moved to single directory
o Group Sources moved to single directory
o Docs moved to doc directory
--akm
Added Pluggable Crypto methods. Any authSource that contains a
cryptPassword method, will have it's method called, otherwise the
method selected by the user is called. --akm
Removed the cryptPassword method from existing Auth Sources. --akm
docLoginRedirect is no longer used. --akm
Changes for 0.20.2
BLAH! I missed some LDAP changes! --akm
Changes for 0.20.1
Fix import problem for pgPropSource --akm
Add performance boost to pgAuthSource and pgPropSource --akm
Make zodbAuthSource.listUsernames return a list. --akm
Update some LDAP Auth source bugs. --akm
Change references to "Authorisation" to "Authentication" since XUF
auth sources authenticate, they don't authorise. --akm
Changed the <h3> tags to <b> tags in the manage_adds.
Changes for 0.20.0
Fix:
https://sourceforge.net/tracker/index.php?func=detail&aid=547327&group_id=36318&atid=416446
https://sourceforge.net/tracker/index.php?func=detail&aid=616485&group_id=36318&atid=416448
https://sourceforge.net/tracker/index.php?func=detail&aid=594081&group_id=36318&atid=416448
https://sourceforge.net/tracker/index.php?func=detail&aid=594526&group_id=36318&atid=416448
Added LDAPAuthSource, based on the auth_ldap module for Apache
(http://www.rudedog.org/auth_ldap/) and the NDS Auth Source of
Phil Harris (AKA ftmpsh). This is only lightly tested, I don't have
the LDAP resources here to test all the features. Binding using uid/
cn and using various filters works (if the userPassword item is
present). This needs more testing by people with better LDAP setups
that I do. --akm
Padded docLoginRedirect to prevent IE from displaying "Friendly" error
messages when -D flag not present when running Zope --akm.
Update UZG to contain entry for LDAPAuthSource. Reformat text
slightly. --akm
Propogate "unable to auth" here requests up. This means the Manager
doesn't get locked out in cookie mode after adding an XUF instance.
It also means that people using a non-existant username at this level
get thrown up a level higher. This might not be what people want to
happen. --akm
Added method makeRedirectPath which is called from docLoginRedirect.
This makes the destination include any querystring that was present
when needing to redirect. -- akm.
Removed some Class globals from exUseFolder.py. These are now set
in __set_state__ if not present in the class so that upgrading users
don't get a crash (hopefully). -- akm.
pgPropSource was losing track of properties under heavy load.
Only noticable if you were setting and deleting a lot of temporary
properties. There is a global property timeout for pgPropSource. --akm
Jason Gibson <jason.gibson@sbcglobal.net> provided a nisAuthSource,
I've added it here --akm.
Refactored validate method to behave a lot more like BasicUserFolder.
Among other things, this fixes the issue where a local role could not
be granted to a user and granted permissions on the same object. --mb
Add NuxUserGroups support (previously on NuxUserGroups_support_branch)
and group sources. --bmh, mb
Now passes authFailedCode to Membership Login Page, The Default Login
Page as defined in the README.Membership will correctly display reason
for login being required --cab
Fixed Edit management pages for user-supplied auth and property
sources --bmh
Removed overriding of __len__ to return the number of users. This was
causing performance problems during authentication. See
http://sourceforge.net/mailarchive/message.php?msg_id=2230743 for
details. WARNING: this means using len(acl_users) to get the number
of users will no longer work! If you were using this trick, please
use len(acl_users.listUsers()) instead. --bmh
Make title property editable --bmh
Make Group Sources changeable dynamically after the acl_users folder has
been created --bmh
Inital import of https Auth source. Also, added a listUsers method
to the zodbBTreeProps source to support listUsers. -- jsb <jonah at cloud9.net>
Changes for 0.10.10
Added mysql Auth and mysql Prop source and mysql.sql schema. Just a
copy of the appropriate pg source with sql that works with myqsl -cab
Fixed negative user cache lookup in std_validade so that it actually
works for users being authenticated thru basic auth, especially if
they're authenticating in outer user folders -- rochael
Made smbAuthSource catch NetBIOSTimeout errors during authentication -- rochael
Fixed dtml/mainUser.dtml to be virtualhost-sensitive when displaying user
icons -- rochael
Updated UZG per user request. Fixed numbering, added information about
addition parameters like Negative Caching.
Changes for 0.10.9
Made dummyZBabelTag compatible to replace the NoBabel in OrderedFolder
while keeping its functionality in XUF -- cab
Changed _doAddUser, _doChangeUser to work with the public interface for
userfolders introduced in Zope2.5. Optional keyword arguments can now
be passed to _doAddUser and _doChangeUser.
PropertySource: Please note that createUser and updateUser, when called
from _doAddUser and _doChangeUser, will no longer be passed a REQUEST,
but a mapping with items from REQUEST updated with those from the
optional keyword arguments. -- pj
Fixed the problem with upgrading from 0.10.7 and below that didn't
account for existing XUF's not having a MessageDialog in their
contents. Now unless specificy replace it will use the MessageDialog
provided. Added how to do that to FAQ and README.Membership --cab
Made docLoginRedirect provide an absolute URL --bmh
MessageDialog in common no longer uses mangage_page_header and
mangage_page_footer v--cab
Changes for 0.10.8
Added the ability for members to change properties, and a default page
in the README.Membership to show how to do it --cab
MessageDialog is now an object in the ZODB that can be changed to fit
the site --cab
Now with 100% guaranteed race-condition-free UserCache goodness! Those
subclassing XUFUser, you will have to change your code. See User.py
for details. --mb
zodbBTreePropSource was returning None instead of the requested
default value, when called with (e.g.) someuser.getProperty('shoesize',13).
(Other property sources didn't have that bug.)
--davidc@debian.org
The tutorial loginform was wrong for Membership in README.Membership
Seems delProperty has never worked.. fixed --akm
Seems delProperty for pgPropSource has never worked.. fixed --akm
Fixed Basic Auth not auth problem. --akm
Fixed Basic Auth not cache problem. --akm
Fixed Cached Users bypassing some auth checks. --akm
Added usPropSource, which allows users to supply property methods TTW.
--bmh
Changes for 0.10.7
PropertyEditor had a typo in dtml and was casting int to None. --zxc
BasicAuth is now broken the other way, it'll allow any user to validate
with any password. --akm
Negative cache checking move was bogus. --akm
redirectToLogin didn't have a security declaration so 2.5.0 refused to
work in cookie mode *sigh* --akm
Fixed the 'None' object has no attribute 'load' setstate errors that
could crop up on propSources, and preemptively took care of the
authSources as well. Also fixed some of the weirder bugs relating to
user object acquisition context. --mb
Bug fixes from sf applied. --akm
Changes for 0.10.6
dummyZBabelTag used the python 2 re, which broke installations using
python 1.5 which still used the now deprecated regex, changed it to
catch the exception and use regex instead for python 1.5, else still
use re --cab
The redirectToLogin without Membership had a little logic problem where it
would basically garantee the existence of a query string, with at least a
lonely question mark even when there was no query string in the original
URL --rochael
smbAuthSource needed to cast NULL role properties to an empty list --akm
smbAuthSource had some dodgey zLOGing in it. --akm
smbAuthSource had some methods that should return [] instead of None. --akm
s/postgres/RADIUS/ in the radiusAuthSource DTML --akm
cookie_validate no longer pulls you from the cache if you're
logging in (which means your cookie wouldn't get set). --akm
Cookies are no longer expired if you're successfully authenticated but
merely unauthorized. --mb
Basic auth resynched with standard user folder, trying to fix
some basic auth issues. --akm.
Negative cache checking now performed outside of the two specific
validate methods. --akm.
A fairly innocuous print debug statement turned into a zLOG at error
level, removed --akm.
Clean up smbAuthSource log messages, and quieten. Only truly
exceptional cases are now logged above BLATHER. --mb
Changes for 0.10.5
Membership redirecting to login was still broken. It should be better
now (twice) --akm
logout() wasn't clearing the advanced cookie. --akm
Negative Cache Value wasn't being passed through to the XUF constructor. --akm
Log Users Out DTML code was broken, should work now. --akm
The User object now contains the authSource as well as the propSource,
making access to roles for custom User-objects possible. --dlk
Following akm's advice, fixed manage_beforeDelete to use two separate
try:except blocks to ensure that if cache-removal fails, deleting
the container.__allow_groups__ property is attempted. This should
fix the problem where deleted xuf instances remain as "ghost" products
causing interference with newer versions of xuf, and also fixes the
problem where deleting a xuf acl_users in a folder makes that folder
inaccessible. --dlk
Fixed cache_delete that was missing the "self" parameter in the method
defintion. --dlk
Fixed xcache_delete that was missing the "self" parameter in the method
definition --akm d8)
These previous two fix the problems with manage_beforeDelete, but, it
will stay the same for now --akm.
Fixed cache_deleteCookieCache that was missing the "self" parameter in
the method defintion. --dlk ;)
Changes for 0.10.4
The instructions for File Based Auth were incorrect in the UZG --akm
redirectToLogin was totally wrong for membership... --akm
docLogin was fixed for VHM use. --akm
Advanced Cookie Mode has changed so that it no longer sends the username
and password. Instead a hash is used as a key into a module level cache.
This should be 100% more secure than standard cookie mode, and removes
the stupid back doors I enabled in the previous version. This work was
based on conversations I had with Stuart Bishop (I basically lifted
the hashing scheme from GUF). This makes use of the Module level cache
code. --akm
There was a code cleanup and a slight reorganisation of some files. --akm
The main User Object has migrated to XUFUser and simarly with the
AnonUser. There is now an empty [Anon]User class that has XUFUser as
it's base. This allows people to create custom User Objects without
jumping through hoops (and simplifies maintaining patches) --akm
Cache Code has changed again. Now there is a module level cache, so
that auth data is shared between threads for a single XUF (thanks to
Stuart Bishop for an enlightening discussion on this and other issues,
and thanks to Chris McDonough for talking me through setting up module
level globals [and sending me some code to work from]) --akm
A Negative User Cache now exists. This is only generally useful for
use with remote auth sources where repeatedly trying to auth non-existant
users is very expensive (where they are authed at a higher level).
You can enable this on creation or from the parameters screen (positive
time in seconds enables). --akm
Domain checking code finally removed. --akm
zodbBTreePropSource changed to be friendlier about users that exist
in remote locations (i.e. aren't create as such through the ZMI). -- akm
Changed some 'print's in the code to use zLOG.LOG
instead. Files affected so far (more to follow): -- rochael
* exUserFolder.py
* basicMemberSource/basicMemberSource.py
* zodbBTreePropSource/zodbBTreePropSource.py
* zodbPropSource/zodbPropSource.py
Changed a couple things in smbAuthSource.py: -- rbanffy
* Method _authenticate_retry now logs several kinds of information
for debugging and diagnostics.
* Modified socket.error handling in _authenticate_retry: changed
"raise" to "return 0".
* Since this generated more problems (failed authentications) than
it solved (our impression it was not right not to return 0 in an
auth fail even due to a communications malfunction), we also
changed socket.error handling to retry no mather what errno tells
us (it said different things for the same problem under Windows
and Linux).
* In order to prevent infinite retries, changed retry handling a
bit. It now retries 3 times. Real-use data will tell us if we
should increase or not retries. To better convey the meaning of
the parameter, changed "retry_depth" to "retries". I strongly
advise the use of credential caching with smbAuthSource, tough, as
it reduces socket errors and load on the domain controllers.
Changes for 0.10.3.1
Readded support for I18N without ZBabel installation, somehow missed
during the transition to SF CVS.
Some text changes as well as an update to the dictionary while we're
at it. No functional changes for this release though.
Changes for 0.10.3
Missed a few LoginRequireds.
Fixed a bug with __allow_groups__ not being set after paste
(probably also not after import).
The sources are now sorted by name in the drop down box..
a BTree version of zodbAuthSource
a BTree version of zodbPropSource
These aren't really all that different to the originals that were
provided by Alex, but, they use BTrees instead of PersistentMappings,
and try to avoid various persistence problems associated with dicts.
Both versions will continue to be supported.
Patches from SF applied.
Advanced Cookie Mode added.
This mode adds a rotor cipher around the cookie. A secret is provided
in order to encode the cookie. The username and password are placed
within a small class which is pickled and then encrypted and then
base64 encoded for transport. There is also a timestamp inside the cookie,
so the ultra-paranoid of you can rotate the cookie based on the timestamp
inside.
Abstracted out the setting and decoding of cookies.
Changes for 0.10.2
all raise 'LoginRequired' <- raise 'Unauthorized'
Raising unauthorizes breaks a million things. CMF people can just
put up with configuring their portal properly.
Radius resynced with version from sourceforge.
manage_tabs redone to be ZBabel'd and to look like standard tabs.
German Language added to the ZBabel dictionary.
Changes for 0.10.1
all raise 'LoginRequired' -> raise 'Unauthorized'
Bug in etcAuthSource listUsers fixed,
and cryptPassword also fixed to get the actual salt.
Zope 2.4.3 has dicked with security settings again.. I've had a round
of permission whacking.
Buggy handling of empty role lists was fixed.
Change to smbAuthSource to use string.lower on usernames for
python 1.5.2 compatibility?
Changes for 0.10.0
Added explicit roles for manage_editUser and friends, to allow
the "Manage users" permission to be useful to non-Manager Users.
Thanks to Heimo Laukkanen <huima@fountainpark.org> for reporting this
one.
zodbAuthSource made more persistent <alex@quad.com.ar>
zodbPropSource was blowing when deleting temporary properties.
XUF is now ZBabel'd which means you can view XUF in different languages
for logging in and installation, if your browser locale is set up.
You will need the latest ZBabel installed. The translation file is in the
I18N directory.
Import this (using Import/Export in ZODB) at the same level as your
ZBabelTower, and then import it from ZBabel. If you have ZBabel installed,
but, your application can't find a ZBabelTower, because of a bug in the
current dtml-fish tag, you might experience some problems. This ZBabel
bug should be fixed sometime soon.
You do not need ZBabel installed to run XUF, XUF installs a dummy
interface for ZBabel so that XUF can continue to run (sorry folks it
defaults to Australian English).
getUserNames() was returning the wrong stuff (notably affected TheJester's
WorkOrders Product)
There is a now an 'Advanced Postgres' Auth Source that uses a seperate
Roles table and a 'more relational' layout. The schema is with the
auth source in pgAuthSourceAlt. Contributed by
Adam Manock <abmanock@earthlink.net>
If you had a membership source and had specified a login page, XUF was
still using the stock docLogin instead of the membership specified page
(for redirectToLogin, exceptions still raise the docLogin).
I changed the icon to something a *little* less hideous
Leonardo Rochael Almeida <leo@hiper.com.br> made the following changes
to smbAuthSource
* Added a 'winsserver' constructor parameter and a '_winsserver'
instance variable to the 'smbAuthSource' class. This variable should
be the empty string, meaning that the authenticaton host will be
looked up by broadcast, or an IP address string pointing to a WINS
server.
* Modified the dtml templates to ask for the above mentioned WINS
server (and also to replace 'Add' with 'Change' in
'manage_editsmbAuthSourceForm').
* Refactored the smbAuthSource class to isolate all smb interaction
inside well defined methods.
Changes for 0.9.0
Messages are now sent back to the docLogin form. There's a file called
LoginRequiredMessages.py where the messages are kept for now (it might
end up a run-time configurable thing later).
There's a new docLogin.dtml file on disk that shows how to use the new
messages. Because docLogin is in the ZODB this won't be automatically
upgraded.
Idle Session Timeouts are in (this is the reason for the minor bump).
If you flick the switch, then users are forced back to the login form
(with a message saying their session timed out), when they're removed
from the cache.
I made some adjustments to the tabs on the management interface because
they were too big, and I cleaned it up a bit for times when they run
together.
The internal API was inconsistent, so that's been updated.
AuthSources no longer need to provide getUsers(), it was never
being called anyway since exUserFolder built it's own.
listUsers now returns the same data as listOneUser, this is used in
other places as if it were a list of listOneUser calls.
Fixed pgAuthSource to deal with NULL rather than empty roles
columns (legacy columns).
Changed Home Directory creation to use copy & paste functions to
copy the skeleton data.
Changes for 0.8.5
I forgot to update the schema file for userproperties to reflect
the temporary properties flag.
Checks for existing cache weren't being performed before removing users
from it, when their data was updated.
Reversed the order for checking in cookie_validate, to allow logging
in as a new user, when session tracking was on. Also now you can
login as a different user, without logging out first, which might
be useful to some people.
etcAuthSource now looks for the correct salt from the file for
encrypting the user supplied password
Changes for 0.8.4
Activating Session Tracking and then adding a new user when there
were none in the XUF was broken.
Changes for 0.8.3
The idle users are flushed from the cache when you ask for the list
of cache users (since it's iterating over the whole list anyway). So
you can manually clear your cache by looking at the Cache Stats page.
If you display the list of logged in users on your site, then your cache
will be flushed for you automagically.
Allowed a destination to be sent to redirectToLogin to allow you to
manually override the destination after logging in.
Added in a __setstate__ for pgPropSource to deal with new ZSQL Methods
being added.
Changes for 0.8.2
A number of bugs related to temp properties fixed in pgPropSource
FTP Access to folders protected with cookie_mode has been fixed, it
now reverts to std_auth (which handles the FTP connection fine), since
FTP auths are handled by getting a "Basic" auth tag coming through, which
should never happen in cookie mode.
This has the knock-on effect of authenticating users that auth from a
higher acl_users that doesn't use cookies, 'more' correctly now. Which is
if you have a user defined above, and in XUF and the XUF user has less
permissions, it'll 401 you if you don't have permissions locally
(which is the correct behaviour). This bit me in the arse when I changed it,
and I'm still leaving it this way. d8)
Users are now flushed from the cache when you edit them (in case you changed
roles), so that new roles should take effect immediately.
The credential cache now uses the (Zope) builtin BTree Module for caching
rather than the AVL Tree implementation. There was a nasty issue with users
appearing multiple times in the AVL Tree which sucked.
There is a report of the Radius Auth Source being broken (most likely
by me), if your radius source stops working, you can try copying the
py-radius.py file from sourceforge over the top of radius.py. If someone
gives me a traceback, I can fix it. I don't seem to be having problems,
but, I don't have a full time RADIUS source either.
Changes for 0.8.1
A bug in _doAddUser was fixed
A bug in the User Object unconditionally calling the prop source was fixed.
Changes for 0.8.0
Experimental "Session Tracking" added (why is it called that? we don't really
track anything, just associate arbitrary data with anonymous users).
This relies on the credential cache being active. Your session will
automatically expire when the anonymous user is so idle that they are
expired from the cache. This is not currently acceptable (to me), but,
it might be to other people, I await feedback on how sessions should expire
gracefully.
Updated the README.txt file to point at the UZG and to explain the
version numbering system.
All this time you couldn't delete properties from a user... who knew?
It's fixed now.
Temporary properties now available, you can setTempProperty() on a
user object, and also flushTempProperties() on a user object.
Temporary properties are accessed like normal properties, and can be
deleted in the same way. flushTempProperties is there to do a quick
flush of all the crap you might have inserted (useful for sessions).
If your user is flushed from the cache, then all temp properties will
also be removed at that point.
Propsource providers should look at the new temp properties stuff and
update accordingly.
Alex provided a whole heap of patches to make basicMembership more usable,
well make it actually work.
Matt Behrens supplied patches to prevent null logins and to allow case
insensitive logins for smbAuthSource
Added a basic FAQ.
Changes for 0.7.10
Active Users type functionality was added. The new function is called
getUserCacheUsers(). It returns a list of dicts;
{'username': theusername, 'lastAccessed': float_value}
lastAccessed represents the last time the user touched something.
The Cache Stats page shows an example usage showing idle time (very cool
I think :-)
The logout method was not correctly removing users from the cache,
although the cookie was removed, so logins were still enforced. I'm not
sure of any side-effects related to it, but,
Some permissions were a little too liberal, including allowing arbitrary
users to set and get Properties on the acl_users folder.
Copy/Paste support for pasting exUserFolders into the root was added.
I'm not sure I like the way this is done. I haven't found any side effects
so far, but, just be wary. Adding an exUserFolder to the root becomes
semi-trivial now. Create one in a sub-folder. Login as the emergency user.
CUT the exUserFolder. Delete the standard acl_users folder. Paste exUserFolder.
You should be away. At least it worked fine for me... YMMV
_doChangeUser and _doDelUsers added so users can be altered and deleted
like for Standard UserFolder.
_createInitialUser added so there should always be your initUser (hopefully)
when you create your exUserFolder.
Emergency User checking brought into line with Standard Folder
__creatable_by_emergency_user_ added and returns 1 to explicitly allow this.
Unenlightened Zopistas Guide updated to have a 'Recipe' like section.
Currently contains a section about adding exUserFolders from python.
Changes for 0.7.9
RADIUS authSource had a problem with non-integers being extracted from
REQUEST (I wish someone at DC would fix this already). I worked around
this problem
Default port for RADIUS is now 1812 in line with the IANA sanctioned list.
Unenlightened Zopistas Guide to exUserFolder version 0.0 included,
covers installation and authentication sources, and the most common
configuration mistake (or misunderstanding).
I almost released with the daggy management screens all Purple or SkyBlue,
so consider yoursevles lucky. This would have been the "Blue" release.
Changes for 0.7.8
zodbPropSource had a bug that must have been there since 0.0.0 where
_p_changed wasn't being called on create, update, or delete user.
Thanks to Bouke Scheurwater for spotting that one.
Alex provided a number of patched to fix a whole bunch of goofy stuff
with Basic Member Source that was stupidly wrong.
Matt Behrens provided a patch to allow emergency user to own exUserFolders
and some of the sources. I've grudgingly updated all the sources to allow
this. It's just a hey nonny nonny to people using it as a root authenticator
now.
Matt Behrens also provided a patch to fix 'broken pipe' problems with
smbAuthSource.
pySMB is now at 0.2 for smbAuthSource WARNING: This will try to use DES
encrypted passwords. Apparently it should be ok if your server doesn't want
them. However if it breaks, unpack the pySMB distribution in the
smbAuthSource directory, there are registry examples there to turn
it off. It unfortunately needs the mxCrypto tools for encrypted passwords
to work. When I've got a bit more time, I'll see if I can make it use
crypt or fcrypt if available instead.
Explicit checks for the emergency user were placed into the cookie_validate
routines. I suspect this may have been the cause of some grief with people
doing weird things like trying to make it the root auth folder.
Changes for 0.7.7
Some Auth sources had problems coping with no roles being selected when
a user was created from the management interface, the stock ones were fixed.
I screwed up some of the DTML, and forgot to change the loading of two of
the methods from the dtml directory.
NO MORE TRACEBACKS ON LOGIN FORMS, there is a little redirector dtml file
dtml/docLoginRedirect that redirects to acl_users/docLogin with destination
set to take them back to where they were going. If you have a custom loginPage
change the redirector dtml to point to your new page.
standard_html swapped for manage_page on Management Pages. Hopefully
this doesn't break someone with an old copy of Zope.
Credential Caching is now available by default for all Authentication Sources,
upgrading installs will get this defaulted to 0 for no caching. You can alter
the cache level from the Parameters Tab. Authors of external sources should
remove any internal auth caching they're doing, and allow the user to decide
how long to cache the credentials for.
Changes for 0.7.6
smbAuthSource included. Doesn't require any external libraries, or compiling.
Uses pySMB from Micheal Teo <michaelteo@bigfoot.com>
Changes for 0.7.5
The Management Interface now batches the user list by 10. This isn't
configurable at the moment (just change the dtml).
The code was re-organised slightly, with all the DTML moving into its
own directory for core.
radiusAuthSource added, but, is so far untested. It is a direct port of
ZRadius for GUF, but, I haven't had a chance to setup a RADIUS server to
test it out.
You can add properties to a user from the management interface.
List Properties on users can be added and edited, if I can work out a decent
way to edit Dicts/Mappings, I'll add that feature in.
This paves the way for defining a set of properties in the Membership
source, so it can create a Signup and Edit page for you automatically.
You will also be able to specify which properties the user can edit, or
roles required to edit a property, this will be in a later release though.
pgPropSource was updated to take into account non-scalar types, and now
pickles all data going into the database, this means ints will stay as ints,
et al.
There is code in there to cope with older properties coming out as strings.
The Schema remains the same.
Changes for 0.7.2
Changes to make it work with older version of python
Some minor bug fixes for membership.
Changes for 0.7.1
DTML Change for cmfPropSource
Changes for 0.7.0
exUserFolder was a little too liberal in removing its cruft, this is now
fixed.
cmfPropSource was provided by Alan Runyan which is a layer around the CMF
property stuff. It's conditionally imported, so if you don't have CMF
installed you don't need to worry that'll it'll break.
Property Sources are optional, and there is a NULL Property Source for this
purpose.
Membership hooks, and a rough start at membership (basicMemberSource),
which has some usable functionality (you MUST read README.Membership before
using this).
Membership Sources are optional and there is a NULL Membership Source for
this purpose.
Changes for 0.6.2
exUserFolder was leaving cruft around when it was being deleted from
Folders. The cruft should now be obliterated if you delete an exUserFolder.
Changes for 0.6.1
Ownership tab enabled, for those sick monkeys that want to use it as a root
Folder (there are some).
fcrypt got the __init__.py that was missing from the 0.6.0 release
zodbAuthSource updated to pull in fcrypt if crypt was missing.
Changes for 0.6.0
Updated for 2.4.1 / Python 2.1
Bug in pgPropSource not deleting users from the property cache fixed.
Bug with Local Roles not getting what it expected fixed.
Alex Verstraeten provided zodbAuthSource, there's a README.zodbAuthSource,
and the same README inside the zodbAuthSource directory.
fcrypt is now included and used if crypt cannot be imported. More information
on fcrypt can be found at http://home.clear.net.nz/pages/c.evans/sw/. This
should help particularly Windows users a lot.
Rudimentary API doc included.
Changes for 0.5.0
A serious bug in zodbPropSource was fixed.
There is now the option of providing a 'Remote Auth' function for
validating. This allows things like IMAP/LDAP auth sources to do their
authentication, since they don't return passwords you can use in general.
There's already a 3rd Party solution that provides IMAP/POP3 authentication,
using the new API.
Changes for 0.4.6
Minor dtml hacks
Changes for 0.4.5
Hooks for 'editing' Authentication and Property Sources were added, along
with the relevant methods in each of the sources.
The management interfaces got a little overhaul, just to make them
a little different (yes I know everything I do looks the same). The two
I didn't want to mess with still have the acquired management interfaces.
A fix for the ZODB Property Source which was missing a few methods.
Changes for 0.4.0
Based on an idea from Martin von Loewis, I added in support for defining
roles for etcAuthSource. This basically uses the current Prop source to
store a 'roles' property. The default role is still there as well for
those of you who might be using it.
Changes for 0.3.0
Adrien Hernot noticed that properties for new users using zodbPropSource
were causing havoc, and that the version.txt file was completely wrong.
Andreas also noticed the version.txt was wrong.
I've been bugged enough by the pair of them to change the single +=
into 1.5.2 compliant syntax.
I don't make any claims about it working under 1.5.2 though.
Changes for 0.2.0
Even more embarassment...
Andreas Heckel provided fixes for some stupid things I left out including;
o Fixing the way I was handling multiple roles coming out of the database
o The wrong icon in the user display
o Alerting me to the fact that pgPropSource didn't actually have a
deleteUsers hook
o Providing a schema for automatically deleting properties in postgres
if you delete a user from the auth source (you have to be using both
pg sources for this to work, and they'd have to be in the same database)
I've put Andreas schema into the distribution, if you want to use
exUserFolder as a straight pgUserFolder, you'll also need to edit
exUserFolder.py and comment out the line indicated in deleteUsers()
Changes for 0.1.0
Pretty embarassing really.
M. Adam Kendall (DaJoker) found some stupid things in the 0.0.0 release
including the fact you couldn't edit user properties, or update them,
or actually change a user in anyway.
I also discovered I was resetting the password to empty if you left it
empty..

View File

@ -0,0 +1,4 @@
import pass_crypt
import pass_md5
import pass_sha
import pass_plain

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,35 @@
2001-05-05 Carey Evans <careye@spamcop.net>
* fcrypt.py: Add module doc string for pydoc, and other globals
for pydoc as well. Add __all__ for Python 2.1, and add
underscores to the front of private variables and functions.
(_set_key): Remove overly clever copying of globals into default
parameters, explicitly copying _shift2 and _skb before the loop.
(_body): Copy _SPtrans explicitly, as above. Remove CR_ENCRYPT
inline function, and reroll unrolled loop using the contents of
this function. Result: more readable code, and a 400% speedup!
(crypt): Add doc string for pydoc and doctest.
(_test): New function for doctest.
* setup.py: Add fields for PKG-INFO metadata.
* README: Add URL of distutils installation manual.
* LICENSE: Add note about license on fcrypt.py being the union of
my license on the Python code and Eric Young's on the original C.
2001-03-24 Carey Evans <careye@spamcop.net>
* setup.py: Move license to separate file. Change email address
to SpamCop forwardder. Update version to 1.1.
* fcrypt.py: Update license text and email address.
(crypt): Fix bug where passwords longer than eight characters were
not truncated.
* README: Update crypt module URL. Remove license text, and add
pointer to LICENSE file. Update email address.
* MANIFEST.in: Add LICENSE, ChangeLog and MANIFEST.in.
* LICENSE: New file.

View File

@ -0,0 +1,77 @@
fcrypt.py copyrights and license
--------------------------------
The Python code by Carey Evans has the following license, which is the
original Python license with the serial numbers filed off, and the
restrictions on advertising removed.
Copyright (C) 2001, 2001 Carey Evans <careye@spamcop.net>
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation.
CAREY EVANS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
EVENT SHALL CAREY EVANS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
The original C code on which this module was based has the following
more restrictive license, so the source for fcrypt.py should be
considered to be covered by the union of my license and Eric Young's.
This library is free for commercial and non-commercial use as long as
the following conditions are aheared to. The following conditions
apply to all code found in this distribution, be it the RC4, RSA,
lhash, DES, etc., code; not just the SSL code. The SSL documentation
included with this distribution is covered by the same copyright terms
except that the holder is Tim Hudson (tjh@mincom.oz.au).
Copyright remains Eric Young's, and as such any Copyright notices in
the code are not to be removed.
If this package is used in a product, Eric Young should be given attribution
as the author of the parts of the library used.
This can be in the form of a textual message at program startup or
in documentation (online or textual) provided with the package.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
"This product includes cryptographic software written by
Eric Young (eay@mincom.oz.au)"
The word 'cryptographic' can be left out if the rouines from the library
being used are not cryptographic related :-).
4. If you include any Windows specific code (or a derivative thereof) from
the apps directory (application code) you must include an acknowledgement:
"This product includes software written by Tim Hudson (tjh@mincom.oz.au)"
THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
The licence and distribution terms for any publically available version or
derivative of this code cannot be changed. i.e. this code cannot simply be
copied and put under another distribution licence
[including the GNU Public Licence.]

View File

@ -0,0 +1 @@
include LICENSE ChangeLog MANIFEST.in

View File

@ -0,0 +1,13 @@
Metadata-Version: 1.0
Name: fcrypt
Version: 1.2
Summary: The Unix password crypt function.
Home-page: http://home.clear.net.nz/pages/c.evans/sw/
Author: Carey Evans
Author-email: careye@spamcop.net
License: BSD
Description: A pure Python implementation of the Unix DES password crypt function,
based on Eric Young's fcrypt.c. It works with any version of Python
from version 1.5 or higher, and because it's pure Python it doesn't
need a C compiler to install it.
Platform: UNKNOWN

View File

@ -0,0 +1,33 @@
fcrypt.py
---------
This is a pure Python implementation of the Unix DES password crypt
function. It was ported from C code by Eric Young (eay@mincom.oz.au).
See the file LICENSE for copyright and license details.
This module is packaged with Distutils. If you have this installed,
or it came with your version of Python, you can install it by typing:
python setup.py install
If not, you can just copy `fcrypt.py' into a directory on your Python
library path, or into the same directory as the program that wants to
use it.
For more information, see the documentation for Python's built-in
crypt module at:
http://www.python.org/doc/current/lib/module-crypt.html
Eric Young's fcrypt.c is available from:
ftp://ftp.psy.uq.oz.au/pub/Crypto/DES/
For more Distutils information, see:
http://www.python.org/doc/current/inst/inst.html
http://www.python.org/sigs/distutils-sig/
--
Carey Evans <careye@spamcop.net>
5 May 2001

View File

@ -0,0 +1 @@
import fcrypt

View File

@ -0,0 +1,602 @@
# fcrypt.py
"""Unix crypt(3) password hash algorithm.
This is a port to Python of the standard Unix password crypt function.
It's a single self-contained source file that works with any version
of Python from version 1.5 or higher. The code is based on Eric
Young's optimised crypt in C.
Python fcrypt is intended for users whose Python installation has not
had the crypt module enabled, or whose C library doesn't include the
crypt function. See the documentation for the Python crypt module for
more information:
http://www.python.org/doc/current/lib/module-crypt.html
The crypt() function is a one-way hash function, intended to hide a
password such that the only way to find out the original password is
to guess values until you get a match. If you need to encrypt and
decrypt data, this is not the module for you.
There are at least two packages providing Python cryptography support:
M2Crypto at <http://www.pobox.org.sg/home/ngps/m2/>, and amkCrypto at
<http://www.amk.ca/python/code/crypto.html>.
Functions:
crypt() -- return hashed password
"""
__author__ = 'Carey Evans <careye@spamcop.net>'
__version__ = '1.2'
__date__ = '6 May 2001'
__credits__ = '''michal j wallace for inspiring me to write this.
Eric Young for the C code this module was copied from.'''
__all__ = ['crypt']
# Copyright (C) 2000, 2001 Carey Evans <careye@spamcop.net>
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and
# that both that copyright notice and this permission notice appear in
# supporting documentation.
#
# CAREY EVANS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
# EVENT SHALL CAREY EVANS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
# Based on C code by Eric Young (eay@mincom.oz.au), which has the
# following copyright. Especially note condition 3, which imposes
# extra restrictions on top of the standard Python license used above.
#
# The fcrypt.c source is available from:
# ftp://ftp.psy.uq.oz.au/pub/Crypto/DES/
# ----- BEGIN fcrypt.c LICENSE -----
#
# This library is free for commercial and non-commercial use as long as
# the following conditions are aheared to. The following conditions
# apply to all code found in this distribution, be it the RC4, RSA,
# lhash, DES, etc., code; not just the SSL code. The SSL documentation
# included with this distribution is covered by the same copyright terms
# except that the holder is Tim Hudson (tjh@mincom.oz.au).
#
# Copyright remains Eric Young's, and as such any Copyright notices in
# the code are not to be removed.
# If this package is used in a product, Eric Young should be given attribution
# as the author of the parts of the library used.
# This can be in the form of a textual message at program startup or
# in documentation (online or textual) provided with the package.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
# must display the following acknowledgement:
# "This product includes cryptographic software written by
# Eric Young (eay@mincom.oz.au)"
# The word 'cryptographic' can be left out if the rouines from the library
# being used are not cryptographic related :-).
# 4. If you include any Windows specific code (or a derivative thereof) from
# the apps directory (application code) you must include an acknowledgement:
# "This product includes software written by Tim Hudson (tjh@mincom.oz.au)"
#
# THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# The licence and distribution terms for any publically available version or
# derivative of this code cannot be changed. i.e. this code cannot simply be
# copied and put under another distribution licence
# [including the GNU Public Licence.]
#
# ----- END fcrypt.c LICENSE -----
import string, struct
_ITERATIONS = 16
_SPtrans = (
# nibble 0
[ 0x00820200, 0x00020000, 0x80800000, 0x80820200,
0x00800000, 0x80020200, 0x80020000, 0x80800000,
0x80020200, 0x00820200, 0x00820000, 0x80000200,
0x80800200, 0x00800000, 0x00000000, 0x80020000,
0x00020000, 0x80000000, 0x00800200, 0x00020200,
0x80820200, 0x00820000, 0x80000200, 0x00800200,
0x80000000, 0x00000200, 0x00020200, 0x80820000,
0x00000200, 0x80800200, 0x80820000, 0x00000000,
0x00000000, 0x80820200, 0x00800200, 0x80020000,
0x00820200, 0x00020000, 0x80000200, 0x00800200,
0x80820000, 0x00000200, 0x00020200, 0x80800000,
0x80020200, 0x80000000, 0x80800000, 0x00820000,
0x80820200, 0x00020200, 0x00820000, 0x80800200,
0x00800000, 0x80000200, 0x80020000, 0x00000000,
0x00020000, 0x00800000, 0x80800200, 0x00820200,
0x80000000, 0x80820000, 0x00000200, 0x80020200 ],
# nibble 1
[ 0x10042004, 0x00000000, 0x00042000, 0x10040000,
0x10000004, 0x00002004, 0x10002000, 0x00042000,
0x00002000, 0x10040004, 0x00000004, 0x10002000,
0x00040004, 0x10042000, 0x10040000, 0x00000004,
0x00040000, 0x10002004, 0x10040004, 0x00002000,
0x00042004, 0x10000000, 0x00000000, 0x00040004,
0x10002004, 0x00042004, 0x10042000, 0x10000004,
0x10000000, 0x00040000, 0x00002004, 0x10042004,
0x00040004, 0x10042000, 0x10002000, 0x00042004,
0x10042004, 0x00040004, 0x10000004, 0x00000000,
0x10000000, 0x00002004, 0x00040000, 0x10040004,
0x00002000, 0x10000000, 0x00042004, 0x10002004,
0x10042000, 0x00002000, 0x00000000, 0x10000004,
0x00000004, 0x10042004, 0x00042000, 0x10040000,
0x10040004, 0x00040000, 0x00002004, 0x10002000,
0x10002004, 0x00000004, 0x10040000, 0x00042000 ],
# nibble 2
[ 0x41000000, 0x01010040, 0x00000040, 0x41000040,
0x40010000, 0x01000000, 0x41000040, 0x00010040,
0x01000040, 0x00010000, 0x01010000, 0x40000000,
0x41010040, 0x40000040, 0x40000000, 0x41010000,
0x00000000, 0x40010000, 0x01010040, 0x00000040,
0x40000040, 0x41010040, 0x00010000, 0x41000000,
0x41010000, 0x01000040, 0x40010040, 0x01010000,
0x00010040, 0x00000000, 0x01000000, 0x40010040,
0x01010040, 0x00000040, 0x40000000, 0x00010000,
0x40000040, 0x40010000, 0x01010000, 0x41000040,
0x00000000, 0x01010040, 0x00010040, 0x41010000,
0x40010000, 0x01000000, 0x41010040, 0x40000000,
0x40010040, 0x41000000, 0x01000000, 0x41010040,
0x00010000, 0x01000040, 0x41000040, 0x00010040,
0x01000040, 0x00000000, 0x41010000, 0x40000040,
0x41000000, 0x40010040, 0x00000040, 0x01010000 ],
# nibble 3
[ 0x00100402, 0x04000400, 0x00000002, 0x04100402,
0x00000000, 0x04100000, 0x04000402, 0x00100002,
0x04100400, 0x04000002, 0x04000000, 0x00000402,
0x04000002, 0x00100402, 0x00100000, 0x04000000,
0x04100002, 0x00100400, 0x00000400, 0x00000002,
0x00100400, 0x04000402, 0x04100000, 0x00000400,
0x00000402, 0x00000000, 0x00100002, 0x04100400,
0x04000400, 0x04100002, 0x04100402, 0x00100000,
0x04100002, 0x00000402, 0x00100000, 0x04000002,
0x00100400, 0x04000400, 0x00000002, 0x04100000,
0x04000402, 0x00000000, 0x00000400, 0x00100002,
0x00000000, 0x04100002, 0x04100400, 0x00000400,
0x04000000, 0x04100402, 0x00100402, 0x00100000,
0x04100402, 0x00000002, 0x04000400, 0x00100402,
0x00100002, 0x00100400, 0x04100000, 0x04000402,
0x00000402, 0x04000000, 0x04000002, 0x04100400 ],
# nibble 4
[ 0x02000000, 0x00004000, 0x00000100, 0x02004108,
0x02004008, 0x02000100, 0x00004108, 0x02004000,
0x00004000, 0x00000008, 0x02000008, 0x00004100,
0x02000108, 0x02004008, 0x02004100, 0x00000000,
0x00004100, 0x02000000, 0x00004008, 0x00000108,
0x02000100, 0x00004108, 0x00000000, 0x02000008,
0x00000008, 0x02000108, 0x02004108, 0x00004008,
0x02004000, 0x00000100, 0x00000108, 0x02004100,
0x02004100, 0x02000108, 0x00004008, 0x02004000,
0x00004000, 0x00000008, 0x02000008, 0x02000100,
0x02000000, 0x00004100, 0x02004108, 0x00000000,
0x00004108, 0x02000000, 0x00000100, 0x00004008,
0x02000108, 0x00000100, 0x00000000, 0x02004108,
0x02004008, 0x02004100, 0x00000108, 0x00004000,
0x00004100, 0x02004008, 0x02000100, 0x00000108,
0x00000008, 0x00004108, 0x02004000, 0x02000008 ],
# nibble 5
[ 0x20000010, 0x00080010, 0x00000000, 0x20080800,
0x00080010, 0x00000800, 0x20000810, 0x00080000,
0x00000810, 0x20080810, 0x00080800, 0x20000000,
0x20000800, 0x20000010, 0x20080000, 0x00080810,
0x00080000, 0x20000810, 0x20080010, 0x00000000,
0x00000800, 0x00000010, 0x20080800, 0x20080010,
0x20080810, 0x20080000, 0x20000000, 0x00000810,
0x00000010, 0x00080800, 0x00080810, 0x20000800,
0x00000810, 0x20000000, 0x20000800, 0x00080810,
0x20080800, 0x00080010, 0x00000000, 0x20000800,
0x20000000, 0x00000800, 0x20080010, 0x00080000,
0x00080010, 0x20080810, 0x00080800, 0x00000010,
0x20080810, 0x00080800, 0x00080000, 0x20000810,
0x20000010, 0x20080000, 0x00080810, 0x00000000,
0x00000800, 0x20000010, 0x20000810, 0x20080800,
0x20080000, 0x00000810, 0x00000010, 0x20080010 ],
# nibble 6
[ 0x00001000, 0x00000080, 0x00400080, 0x00400001,
0x00401081, 0x00001001, 0x00001080, 0x00000000,
0x00400000, 0x00400081, 0x00000081, 0x00401000,
0x00000001, 0x00401080, 0x00401000, 0x00000081,
0x00400081, 0x00001000, 0x00001001, 0x00401081,
0x00000000, 0x00400080, 0x00400001, 0x00001080,
0x00401001, 0x00001081, 0x00401080, 0x00000001,
0x00001081, 0x00401001, 0x00000080, 0x00400000,
0x00001081, 0x00401000, 0x00401001, 0x00000081,
0x00001000, 0x00000080, 0x00400000, 0x00401001,
0x00400081, 0x00001081, 0x00001080, 0x00000000,
0x00000080, 0x00400001, 0x00000001, 0x00400080,
0x00000000, 0x00400081, 0x00400080, 0x00001080,
0x00000081, 0x00001000, 0x00401081, 0x00400000,
0x00401080, 0x00000001, 0x00001001, 0x00401081,
0x00400001, 0x00401080, 0x00401000, 0x00001001 ],
# nibble 7
[ 0x08200020, 0x08208000, 0x00008020, 0x00000000,
0x08008000, 0x00200020, 0x08200000, 0x08208020,
0x00000020, 0x08000000, 0x00208000, 0x00008020,
0x00208020, 0x08008020, 0x08000020, 0x08200000,
0x00008000, 0x00208020, 0x00200020, 0x08008000,
0x08208020, 0x08000020, 0x00000000, 0x00208000,
0x08000000, 0x00200000, 0x08008020, 0x08200020,
0x00200000, 0x00008000, 0x08208000, 0x00000020,
0x00200000, 0x00008000, 0x08000020, 0x08208020,
0x00008020, 0x08000000, 0x00000000, 0x00208000,
0x08200020, 0x08008020, 0x08008000, 0x00200020,
0x08208000, 0x00000020, 0x00200020, 0x08008000,
0x08208020, 0x00200000, 0x08200000, 0x08000020,
0x00208000, 0x00008020, 0x08008020, 0x08200000,
0x00000020, 0x08208000, 0x00208020, 0x00000000,
0x08000000, 0x08200020, 0x00008000, 0x00208020 ] )
_skb = (
# for C bits (numbered as per FIPS 46) 1 2 3 4 5 6
[ 0x00000000, 0x00000010, 0x20000000, 0x20000010,
0x00010000, 0x00010010, 0x20010000, 0x20010010,
0x00000800, 0x00000810, 0x20000800, 0x20000810,
0x00010800, 0x00010810, 0x20010800, 0x20010810,
0x00000020, 0x00000030, 0x20000020, 0x20000030,
0x00010020, 0x00010030, 0x20010020, 0x20010030,
0x00000820, 0x00000830, 0x20000820, 0x20000830,
0x00010820, 0x00010830, 0x20010820, 0x20010830,
0x00080000, 0x00080010, 0x20080000, 0x20080010,
0x00090000, 0x00090010, 0x20090000, 0x20090010,
0x00080800, 0x00080810, 0x20080800, 0x20080810,
0x00090800, 0x00090810, 0x20090800, 0x20090810,
0x00080020, 0x00080030, 0x20080020, 0x20080030,
0x00090020, 0x00090030, 0x20090020, 0x20090030,
0x00080820, 0x00080830, 0x20080820, 0x20080830,
0x00090820, 0x00090830, 0x20090820, 0x20090830 ],
# for C bits (numbered as per FIPS 46) 7 8 10 11 12 13
[ 0x00000000, 0x02000000, 0x00002000, 0x02002000,
0x00200000, 0x02200000, 0x00202000, 0x02202000,
0x00000004, 0x02000004, 0x00002004, 0x02002004,
0x00200004, 0x02200004, 0x00202004, 0x02202004,
0x00000400, 0x02000400, 0x00002400, 0x02002400,
0x00200400, 0x02200400, 0x00202400, 0x02202400,
0x00000404, 0x02000404, 0x00002404, 0x02002404,
0x00200404, 0x02200404, 0x00202404, 0x02202404,
0x10000000, 0x12000000, 0x10002000, 0x12002000,
0x10200000, 0x12200000, 0x10202000, 0x12202000,
0x10000004, 0x12000004, 0x10002004, 0x12002004,
0x10200004, 0x12200004, 0x10202004, 0x12202004,
0x10000400, 0x12000400, 0x10002400, 0x12002400,
0x10200400, 0x12200400, 0x10202400, 0x12202400,
0x10000404, 0x12000404, 0x10002404, 0x12002404,
0x10200404, 0x12200404, 0x10202404, 0x12202404 ],
# for C bits (numbered as per FIPS 46) 14 15 16 17 19 20
[ 0x00000000, 0x00000001, 0x00040000, 0x00040001,
0x01000000, 0x01000001, 0x01040000, 0x01040001,
0x00000002, 0x00000003, 0x00040002, 0x00040003,
0x01000002, 0x01000003, 0x01040002, 0x01040003,
0x00000200, 0x00000201, 0x00040200, 0x00040201,
0x01000200, 0x01000201, 0x01040200, 0x01040201,
0x00000202, 0x00000203, 0x00040202, 0x00040203,
0x01000202, 0x01000203, 0x01040202, 0x01040203,
0x08000000, 0x08000001, 0x08040000, 0x08040001,
0x09000000, 0x09000001, 0x09040000, 0x09040001,
0x08000002, 0x08000003, 0x08040002, 0x08040003,
0x09000002, 0x09000003, 0x09040002, 0x09040003,
0x08000200, 0x08000201, 0x08040200, 0x08040201,
0x09000200, 0x09000201, 0x09040200, 0x09040201,
0x08000202, 0x08000203, 0x08040202, 0x08040203,
0x09000202, 0x09000203, 0x09040202, 0x09040203 ],
# for C bits (numbered as per FIPS 46) 21 23 24 26 27 28
[ 0x00000000, 0x00100000, 0x00000100, 0x00100100,
0x00000008, 0x00100008, 0x00000108, 0x00100108,
0x00001000, 0x00101000, 0x00001100, 0x00101100,
0x00001008, 0x00101008, 0x00001108, 0x00101108,
0x04000000, 0x04100000, 0x04000100, 0x04100100,
0x04000008, 0x04100008, 0x04000108, 0x04100108,
0x04001000, 0x04101000, 0x04001100, 0x04101100,
0x04001008, 0x04101008, 0x04001108, 0x04101108,
0x00020000, 0x00120000, 0x00020100, 0x00120100,
0x00020008, 0x00120008, 0x00020108, 0x00120108,
0x00021000, 0x00121000, 0x00021100, 0x00121100,
0x00021008, 0x00121008, 0x00021108, 0x00121108,
0x04020000, 0x04120000, 0x04020100, 0x04120100,
0x04020008, 0x04120008, 0x04020108, 0x04120108,
0x04021000, 0x04121000, 0x04021100, 0x04121100,
0x04021008, 0x04121008, 0x04021108, 0x04121108 ],
# for D bits (numbered as per FIPS 46) 1 2 3 4 5 6
[ 0x00000000, 0x10000000, 0x00010000, 0x10010000,
0x00000004, 0x10000004, 0x00010004, 0x10010004,
0x20000000, 0x30000000, 0x20010000, 0x30010000,
0x20000004, 0x30000004, 0x20010004, 0x30010004,
0x00100000, 0x10100000, 0x00110000, 0x10110000,
0x00100004, 0x10100004, 0x00110004, 0x10110004,
0x20100000, 0x30100000, 0x20110000, 0x30110000,
0x20100004, 0x30100004, 0x20110004, 0x30110004,
0x00001000, 0x10001000, 0x00011000, 0x10011000,
0x00001004, 0x10001004, 0x00011004, 0x10011004,
0x20001000, 0x30001000, 0x20011000, 0x30011000,
0x20001004, 0x30001004, 0x20011004, 0x30011004,
0x00101000, 0x10101000, 0x00111000, 0x10111000,
0x00101004, 0x10101004, 0x00111004, 0x10111004,
0x20101000, 0x30101000, 0x20111000, 0x30111000,
0x20101004, 0x30101004, 0x20111004, 0x30111004 ],
# for D bits (numbered as per FIPS 46) 8 9 11 12 13 14
[ 0x00000000, 0x08000000, 0x00000008, 0x08000008,
0x00000400, 0x08000400, 0x00000408, 0x08000408,
0x00020000, 0x08020000, 0x00020008, 0x08020008,
0x00020400, 0x08020400, 0x00020408, 0x08020408,
0x00000001, 0x08000001, 0x00000009, 0x08000009,
0x00000401, 0x08000401, 0x00000409, 0x08000409,
0x00020001, 0x08020001, 0x00020009, 0x08020009,
0x00020401, 0x08020401, 0x00020409, 0x08020409,
0x02000000, 0x0A000000, 0x02000008, 0x0A000008,
0x02000400, 0x0A000400, 0x02000408, 0x0A000408,
0x02020000, 0x0A020000, 0x02020008, 0x0A020008,
0x02020400, 0x0A020400, 0x02020408, 0x0A020408,
0x02000001, 0x0A000001, 0x02000009, 0x0A000009,
0x02000401, 0x0A000401, 0x02000409, 0x0A000409,
0x02020001, 0x0A020001, 0x02020009, 0x0A020009,
0x02020401, 0x0A020401, 0x02020409, 0x0A020409 ],
# for D bits (numbered as per FIPS 46) 16 17 18 19 20 21
[ 0x00000000, 0x00000100, 0x00080000, 0x00080100,
0x01000000, 0x01000100, 0x01080000, 0x01080100,
0x00000010, 0x00000110, 0x00080010, 0x00080110,
0x01000010, 0x01000110, 0x01080010, 0x01080110,
0x00200000, 0x00200100, 0x00280000, 0x00280100,
0x01200000, 0x01200100, 0x01280000, 0x01280100,
0x00200010, 0x00200110, 0x00280010, 0x00280110,
0x01200010, 0x01200110, 0x01280010, 0x01280110,
0x00000200, 0x00000300, 0x00080200, 0x00080300,
0x01000200, 0x01000300, 0x01080200, 0x01080300,
0x00000210, 0x00000310, 0x00080210, 0x00080310,
0x01000210, 0x01000310, 0x01080210, 0x01080310,
0x00200200, 0x00200300, 0x00280200, 0x00280300,
0x01200200, 0x01200300, 0x01280200, 0x01280300,
0x00200210, 0x00200310, 0x00280210, 0x00280310,
0x01200210, 0x01200310, 0x01280210, 0x01280310 ],
# for D bits (numbered as per FIPS 46) 22 23 24 25 27 28
[ 0x00000000, 0x04000000, 0x00040000, 0x04040000,
0x00000002, 0x04000002, 0x00040002, 0x04040002,
0x00002000, 0x04002000, 0x00042000, 0x04042000,
0x00002002, 0x04002002, 0x00042002, 0x04042002,
0x00000020, 0x04000020, 0x00040020, 0x04040020,
0x00000022, 0x04000022, 0x00040022, 0x04040022,
0x00002020, 0x04002020, 0x00042020, 0x04042020,
0x00002022, 0x04002022, 0x00042022, 0x04042022,
0x00000800, 0x04000800, 0x00040800, 0x04040800,
0x00000802, 0x04000802, 0x00040802, 0x04040802,
0x00002800, 0x04002800, 0x00042800, 0x04042800,
0x00002802, 0x04002802, 0x00042802, 0x04042802,
0x00000820, 0x04000820, 0x00040820, 0x04040820,
0x00000822, 0x04000822, 0x00040822, 0x04040822,
0x00002820, 0x04002820, 0x00042820, 0x04042820,
0x00002822, 0x04002822, 0x00042822, 0x04042822 ] )
_shifts2 = (0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0)
_con_salt = [
0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,
0xDA,0xDB,0xDC,0xDD,0xDE,0xDF,0xE0,0xE1,
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,
0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,0xF1,
0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,
0xFA,0xFB,0xFC,0xFD,0xFE,0xFF,0x00,0x01,
0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,
0x0A,0x0B,0x05,0x06,0x07,0x08,0x09,0x0A,
0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,
0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,
0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,
0x23,0x24,0x25,0x20,0x21,0x22,0x23,0x24,
0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,
0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0x34,
0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,
0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44 ]
_cov_2char = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
def _HPERM_OP(a):
"""Clever bit manipulation."""
t = ((a << 18) ^ a) & 0xcccc0000
return a ^ t ^ ((t >> 18) & 0x3fff)
def _PERM_OP(a,b,n,m):
"""Cleverer bit manipulation."""
t = ((a >> n) ^ b) & m
b = b ^ t
a = a ^ (t << n)
return a,b
def _set_key(password):
"""Generate DES key schedule from ASCII password."""
c,d = struct.unpack('<ii', password)
c = (c & 0x7f7f7f7f) << 1
d = (d & 0x7f7f7f7f) << 1
d,c = _PERM_OP(d,c,4,0x0f0f0f0f)
c = _HPERM_OP(c)
d = _HPERM_OP(d)
d,c = _PERM_OP(d,c,1,0x55555555)
c,d = _PERM_OP(c,d,8,0x00ff00ff)
d,c = _PERM_OP(d,c,1,0x55555555)
# Any sign-extended bits are masked off.
d = (((d & 0x000000ff) << 16) | (d & 0x0000ff00) |
((d & 0x00ff0000) >> 16) | ((c >> 4) & 0x0f000000))
c = c & 0x0fffffff
# Copy globals into local variables for loop.
shifts2 = _shifts2
skbc0, skbc1, skbc2, skbc3, skbd0, skbd1, skbd2, skbd3 = _skb
k = [0] * (_ITERATIONS * 2)
for i in range(_ITERATIONS):
# Only operates on top 28 bits.
if shifts2[i]:
c = (c >> 2) | (c << 26)
d = (d >> 2) | (d << 26)
else:
c = (c >> 1) | (c << 27)
d = (d >> 1) | (d << 27)
c = c & 0x0fffffff
d = d & 0x0fffffff
s = ( skbc0[ c & 0x3f ] |
skbc1[((c>> 6) & 0x03) | ((c>> 7) & 0x3c)] |
skbc2[((c>>13) & 0x0f) | ((c>>14) & 0x30)] |
skbc3[((c>>20) & 0x01) |
((c>>21) & 0x06) | ((c>>22) & 0x38)] )
t = ( skbd0[ d & 0x3f ] |
skbd1[((d>> 7) & 0x03) | ((d>> 8) & 0x3c)] |
skbd2[((d>>15) & 0x3f) ] |
skbd3[((d>>21) & 0x0f) | ((d>>22) & 0x30)] )
k[2*i] = ((t << 16) | (s & 0x0000ffff)) & 0xffffffff
s = (s >> 16) | (t & 0xffff0000)
# Top bit of s may be 1.
s = (s << 4) | ((s >> 28) & 0x0f)
k[2*i + 1] = s & 0xffffffff
return k
def _body(ks, E0, E1):
"""Use the key schedule ks and salt E0, E1 to create the password hash."""
# Copy global variable into locals for loop.
SP0, SP1, SP2, SP3, SP4, SP5, SP6, SP7 = _SPtrans
inner = range(0, _ITERATIONS*2, 2)
l = r = 0
for j in range(25):
l,r = r,l
for i in inner:
t = r ^ ((r >> 16) & 0xffff)
u = t & E0
t = t & E1
u = u ^ (u << 16) ^ r ^ ks[i]
t = t ^ (t << 16) ^ r ^ ks[i+1]
t = ((t >> 4) & 0x0fffffff) | (t << 28)
l,r = r,(SP1[(t ) & 0x3f] ^ SP3[(t>> 8) & 0x3f] ^
SP5[(t>>16) & 0x3f] ^ SP7[(t>>24) & 0x3f] ^
SP0[(u ) & 0x3f] ^ SP2[(u>> 8) & 0x3f] ^
SP4[(u>>16) & 0x3f] ^ SP6[(u>>24) & 0x3f] ^ l)
l = ((l >> 1) & 0x7fffffff) | ((l & 0x1) << 31)
r = ((r >> 1) & 0x7fffffff) | ((r & 0x1) << 31)
r,l = _PERM_OP(r, l, 1, 0x55555555)
l,r = _PERM_OP(l, r, 8, 0x00ff00ff)
r,l = _PERM_OP(r, l, 2, 0x33333333)
l,r = _PERM_OP(l, r, 16, 0x0000ffff)
r,l = _PERM_OP(r, l, 4, 0x0f0f0f0f)
return l,r
def crypt(password, salt):
"""Generate an encrypted hash from the passed password. If the password
is longer than eight characters, only the first eight will be used.
The first two characters of the salt are used to modify the encryption
algorithm used to generate in the hash in one of 4096 different ways.
The characters for the salt must be alphanumeric, '.' or '/'.
The returned hash begins with the two characters of the salt, and
should be passed as the salt to verify the password.
Example:
>>> from fcrypt import crypt
>>> password = 'AlOtBsOl'
>>> salt = 'cE'
>>> hash = crypt(password, salt)
>>> hash
'cEpWz5IUCShqM'
>>> crypt(password, hash) == hash
1
>>> crypt('IaLaIoK', hash) == hash
0
In practice, you would read the password using something like the
getpass module, and generate the salt randomly:
>>> import random, string
>>> saltchars = string.letters + string.digits + './'
>>> salt = random.choice(saltchars) + random.choice(saltchars)
"""
if len(salt) < 2:
salt = salt + 'AA'
Eswap0 = _con_salt[ord(salt[0])]
Eswap1 = _con_salt[ord(salt[1])] << 4
ks = _set_key((password + '\0\0\0\0\0\0\0\0')[:8])
out1,out2 = _body(ks, Eswap0, Eswap1)
# Convert numbers to big-endian...
be1, be2 = struct.unpack('>ii', struct.pack('<ii', out1, out2))
# then extract 24-bit subsets.
b24 = [(be1 >> 8) & 0xffffff,
((be1 << 16) & 0xff0000) | ((be2 >> 16) & 0xffff),
(be2 << 8) & 0xffff00]
# Convert to ASCII encoding, 4 characters for each 24 bits.
res = [salt[0], salt[1]]
for b in b24:
for i in range(18, -6, -6):
res.append(_cov_2char[(b >> i) & 0x3f])
return string.join(res[:13], '')
def _test():
"""Run doctest on fcrypt module."""
import doctest, fcrypt
return doctest.testmod(fcrypt)
if __name__ == '__main__':
_test()

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
# distutils setup script for fcrypt.
#
# Copyright (C) 2000, 2001 Carey Evans <careye@spamcop.net>
from distutils.core import setup
setup( name = 'fcrypt',
version = '1.2',
description = 'The Unix password crypt function.',
author = 'Carey Evans',
author_email = 'careye@spamcop.net',
url = 'http://home.clear.net.nz/pages/c.evans/sw/',
licence = 'BSD',
long_description = """\
A pure Python implementation of the Unix DES password crypt function,
based on Eric Young's fcrypt.c. It works with any version of Python
from version 1.5 or higher, and because it's pure Python it doesn't
need a C compiler to install it.""",
py_modules = ['fcrypt'] )

View File

@ -0,0 +1,44 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: pass_crypt.py,v 1.3 2004/11/18 09:24:46 akm Exp $
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import CryptoPluginRegister
try:
from crypt import crypt
except:
from fcrypt.fcrypt import crypt
def cryptPassword(authSource, username, password):
u = authSource.listOneUser(username)
if not u:
salt = username[:2]
else:
salt=u[0]['password'][:2]
secret = crypt(password, salt)
return secret
CryptPlugin=CryptoPluginRegister('Crypt', 'crypt', 'Crypt', cryptPassword)
exUserFolder.cryptoSources['Crypt']=CryptPlugin

View File

@ -0,0 +1,47 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: pass_md5.py,v 1.1 2004/11/10 14:15:52 akm Exp $
import md5, base64, string
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import CryptoPluginRegister
# Simple digest
def cryptPassword(authSource, username, password):
digest = md5.new()
digest.update(password)
digest = digest.digest()
secret = string.strip(base64.encodestring(digest))
return secret
# Digest includes username
# So two passwords for different users hash differently
def cryptPassword2(authSource, username, password):
newPass = username+':'+password
return cryptPassword(authSource, username, newPass)
MD5Plugin1=CryptoPluginRegister('MD51', 'MD5', 'MD5 Password Only', cryptPassword)
exUserFolder.cryptoSources['MD51']=MD5Plugin1
MD5Plugin2=CryptoPluginRegister('MD52', 'MD5', 'MD5 Username + Password', cryptPassword2)
exUserFolder.cryptoSources['MD52']=MD5Plugin2

View File

@ -0,0 +1,31 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: pass_plain.py,v 1.1 2004/11/10 14:15:52 akm Exp $
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import CryptoPluginRegister
# Simple digest
def cryptPassword(authSource, username, password):
return password
PlainPlugin=CryptoPluginRegister('Plaintext', 'Plaintext', 'No Encryption', cryptPassword)
exUserFolder.cryptoSources['Plaintext']=PlainPlugin

View File

@ -0,0 +1,41 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: pass_sha.py,v 1.1 2004/11/10 14:15:52 akm Exp $
import sha
from base64 import encodestring
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import CryptoPluginRegister
def cryptPassword(authSource, username, password):
return encodestring(sha.new(password).digest())
def cryptPassword2(authSource, username, password):
newPass = username+':'+password
return cryptPassword(authSource, username, newPass)
SHAPlugin1=CryptoPluginRegister('SHA1', 'SHA', 'SHA Password Only', cryptPassword)
exUserFolder.cryptoSources['SHA1']=SHAPlugin1
SHAPlugin2=CryptoPluginRegister('SHA2', 'SHA', 'SHA Username + Password', cryptPassword2)
exUserFolder.cryptoSources['SHA2']=SHAPlugin2

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,20 @@
# This script interrogates the old-skool NuxUserGroups_support_branch
# group structure and outputs a tab-delimited file you can send to
# loadOldGroups. Just in case anyone is using it. :-)
#
# Matt Behrens <matt.behrens@kohler.com>
def getOldGroups(self):
"Reconstruct a group list from the old-style _groups property"
from string import join
props = self.currentPropSource.userProperties
groups = {}
for username in props.keys():
for groupname in props[username].getProperty('_groups', ()):
if not groups.has_key(groupname):
groups[groupname] = []
groups[groupname].append(username)
out = ''
for groupname in groups.keys():
out = out + '%s %s\n' % (groupname, join(groups[groupname], ' '))
return out

View File

@ -0,0 +1,26 @@
# This takes 'old_groups.txt' from var (create it using getOldGroups)
# and sets up all the groups therein using NuxUserGroups calls. This
# will load a group source if you need to do such a thing.
#
# Matt Behrens <matt.behrens@kohler.com>
def loadOldGroups(self):
from os.path import join as pathJoin
from string import split, strip
groups_file = open(pathJoin(CLIENT_HOME, 'old_groups.txt'), 'r')
out = ''
for group_line in groups_file.readlines():
group_line_elements = split(strip(group_line), ' ')
group_name = group_line_elements[0]
group_members = group_line_elements[1:]
if self.getGroupById(group_name, default=None) is None:
out = out + 'adding group %s\n' % group_name
self.userFolderAddGroup(group_name)
out = out + 'setting group %s membership to %s\n' % (group_name, group_members)
self.setUsersOfGroup(group_members, group_name)
return out

View File

@ -0,0 +1,140 @@
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: usAuthSourceMethods.py,v 1.3 2001/12/01 08:40:04 akm Exp $
#
########################################################################
#
# This is an example of an Extension Module to provide User Supplied
# Authentication Methods.
#
# It mimics the behaviour of the pgAuthSource Module, and the sql queries
# Used here would be added as ZSQLMethods in the usAuthSource Folder.
# (you can basically cut and paste them from the bottom of this .py file
# into the ZSQL Method Template Area
#
# It's not complete, but, you do get the idea...
#
# Each function becomes usFunctionName
#
# e.g. listOneUser -> usListOneUser
#
import string
from crypt import crypt
def listOneUser(self,username):
users = []
result=self.sqlListOneUser(username=username)
for n in result:
username=sqlattr(n,'username')
password=sqlattr(n,'password')
roles=string.split(sqlattr(n,'roles'))
N={'username':username, 'password':password, 'roles':roles}
users.append(N)
return users
def listUsers(self):
"""Returns a list of user names or [] if no users exist"""
users = []
result=self.sqlListUsers()
for n in result:
username=sqlattr(n,'username')
N={'username':username}
users.append(N)
return users
def getUsers(self):
"""Return a list of user objects or [] if no users exist"""
data=[]
try: items=self.listusers()
except: return data
for people in items:
roles=string.split(people['roles'],',')
user=User(people['username'], roles, '')
data.append(user)
return data
def cryptPassword(self, username, password):
salt =username[:2]
secret = crypt(password, salt)
return secret
def deleteUsers(self, userids):
for uid in userids:
self.sqlDeleteOneUser(userid=uid)
# Helper Functions...
from string import upper, lower
import Missing
mt=type(Missing.Value)
def typeconv(val):
if type(val)==mt:
return ''
return val
def sqlattr(ob, attr):
name=attr
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
attr=upper(attr)
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
attr=lower(attr)
if hasattr(ob, attr):
return typeconv(getattr(ob, attr))
raise NameError, name
########################################################################
# SQL METHODS USED ABOVE
# PASTE INTO ZSQL METHODS
# take note of what parameters are used in each query
########################################################################
_sqlListUsers="""
SELECT * FROM passwd
"""
_sqlListOneUser="""
SELECT * FROM passwd
where username=<dtml-sqlvar username type=string>
"""
_sqlDeleteOneUser="""
DELETE FROM passwd
where uid=<dtml-sqlvar userid type=int>
"""
_sqlInsertUser="""
INSERT INTO passwd (username, password, roles)
VALUES (<dtml-sqlvar username type=string>,
<dtml-sqlvar password type=string>,
<dtml-sqlvar roles type=string>)
"""
_sqlUpdateUserPassword="""
UPDATE passwd set password=<dtml-sqlvar password type=string>
WHERE username=<dtml-sqlvar username type=string>
"""
_sqlUpdateUser="""
UPDATE passwd set roles=<dtml-sqlvar roles type=string>
WHERE username=<dtml-sqlvar username type=string>
"""

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,32 @@
#
# Extensible User Folder
#
# Null Group Source for exUserFolder
#
# Author: Brent Hendricks <bmh@users.sourceforge.net>
# $Id: GroupSource.py,v 1.1 2002/12/02 23:20:49 bmh Exp $
from Globals import DTMLFile
manage_addGroupSourceForm=DTMLFile('manage_addGroupSourceForm', globals(), __name__='manage_addGroupSourceForm')
def manage_addGroupSource(dispatcher, REQUEST):
""" Add a Group Source """
# Get the XUF object we're being added to
xuf = dispatcher.Destination()
groupId = REQUEST.get('groupId', None)
if groupId:
# Invoke the add method for this plugin
xuf.groupSources[groupId].manage_addMethod(xuf, REQUEST)
else:
raise "BadRequest", "Required parameter 'groupId' omitted"
dispatcher.manage_main(dispatcher, REQUEST)
class GroupSource:
pass

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2002/12/02 23:20:49 bmh Exp $
import GroupSource

View File

@ -0,0 +1,33 @@
<dtml-var manage_page_header>
<dtml-if currentGroupSource>
<dtml-var "MessageDialog(title='Group Source Exists', message='Error: There is already a group source here. Please delete it first', action='manage_main')">
<dtml-elif allDone>
<dtml-var expr="manage_addGroupSource(REQUEST)">
<dtml-elif groupId>
<dtml-call "REQUEST.set('groupForm',doGroupSourceForm(groupId=groupId))">
<dtml-var "groupForm(mapping=_)">
<dtml-else>
<dtml-var "DialogHeader(_.None,_,DialogTitle='Add eXtensible User Folder Group Source')">
<form action="&dtml-URL;" method="post">
<table cellspacing="2">
<tr>
<td align="left" valign="top">
<b><dtml-babel src="'en'">Group Source</dtml-babel></b>
</td>
<td>
<select name="groupId">
<dtml-in getGroupSources sort="name">
<option value="<dtml-var "_['sequence-item'].name">"><dtml-var description></option>
</dtml-in>
</select>
</td>
</tr>
<tr>
<td></td>
<td><br><input type="submit" value=" <dtml-babel src="'en'">Add</dtml-babel> "></td>
</tr>
</table>
</form>
<dtml-var DialogFooter>
</dtml-if>
<dtml-var manage_page_footer>

View File

@ -0,0 +1,31 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: __init__.py,v 1.1 2004/11/10 14:15:53 akm Exp $
import nullGroupSource
# If this fails due to NUG being absent, just skip it
try:
import zodbGroupSource
except ImportError:
pass

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:53 akm Exp $
import nullGroupSource

View File

@ -0,0 +1,21 @@
<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Group Source', dialog_width='')">
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
<dtml-let listVar=sequence-item>
<dtml-in "REQUEST[listVar]">
<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
</dtml-in>
</dtml-let>
<dtml-else>
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-if>
</dtml-in>
<input type="HIDDEN" name="allDone" value="1">
<b><dtml-babel src="'en'">This Group Source has no configuration Items</dtml-babel></b><br>
<br>
<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,34 @@
#
# Extensible User Folder
#
# Null Group Source for exUserFolder
#
# Author: Brent Hendricks <bmh@users.sourceforge.net>
# $Id: nullGroupSource.py,v 1.1 2004/11/10 14:15:53 akm Exp $
from Globals import HTMLFile, INSTANCE_HOME
from OFS.Folder import Folder
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
from Products.exUserFolder.nullPlugin import nullPlugin
def manage_addNullGroupSource(self, REQUEST):
""" Add a Group Source """
self.currentGroupSource=None
return ''
manage_addNullGroupSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
manage_editNullGroupSourceForm=None
nullGroupReg=PluginRegister('nullGroupSource',
'Null Group Source',
nullPlugin,
manage_addNullGroupSourceForm,
manage_addNullGroupSource,
manage_editNullGroupSourceForm)
exUserFolder.groupSources['nullGroupSource']=nullGroupReg

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:54 akm Exp $
import zodbGroupSource

View File

@ -0,0 +1,21 @@
<dtml-var "DialogHeader(_.None,_,DialogTitle='Add ZODB Group Source')">
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
<dtml-let listVar=sequence-item>
<dtml-in "REQUEST[listVar]">
<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
</dtml-in>
</dtml-let>
<dtml-else>
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-if>
</dtml-in>
<input type="HIDDEN" name="allDone" value="1">
<b><dtml-babel src="'en'">This group source requires no user configuration items at this time.</dtml-babel></b><br>
<INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">NEXT</dtml-babel> ">
</FORM>
<dtml-var DialogFooter>

View File

@ -0,0 +1,7 @@
<dtml-var "DialogHeader(_.None,_,DialogTitle='ZODB Group Source',dialog_width='100%')">
<dtml-var manage_tabs>
<FORM ACTION="manage_main" METHOD="POST">
<b><dtml-babel src="'en'">This group source requires no user configuration items at this time.</dtml-babel></b><br>
<INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">OK</dtml-babel> ">
</FORM>
<dtml-var DialogFooter>

View File

@ -0,0 +1,177 @@
#
# Extensible User Folder
#
# ZODB Group Source for exUserFolder
#
# Author: Brent Hendricks <mh@users.sourceforge.net>
# $Id: zodbGroupSource.py,v 1.1 2004/11/10 14:15:54 akm Exp $
from Globals import HTMLFile, MessageDialog, INSTANCE_HOME,Acquisition, PersistentMapping
from OFS.Folder import Folder
from Products.ZSQLMethods.SQL import SQL
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
from Products.NuxUserGroups.UserFolderWithGroups import Group, _marker
import time
import zLOG
import sys
manage_addGroupSourceForm=HTMLFile('manage_addzodbGroupSourceForm', globals())
def manage_addzodbGroupSource(self, REQUEST):
""" Add a ZODB Group Source """
o = zodbGroupSource()
self._setObject('zodbGroupSource', o, None, None, 0)
o = getattr(self, 'zodbGroupSource')
# Allow Prop Source to setup default users...
if hasattr(o, 'postInitialisation'):
o.postInitialisation(REQUEST)
self.currentGroupSource=o
manage_addzodbGroupSourceForm=HTMLFile('manage_addzodbGroupSourceForm', globals())
manage_editzodbGroupSourceForm=HTMLFile('manage_editzodbGroupSourceForm', globals())
#
# Very very simple thing, used as an example of how to write a property source
# Not recommended for large scale production sites...
#
class zodbGroupSource(Folder):
""" Store Group Data inside ZODB, the simplistic way """
meta_type='Group Source'
title='Simplistic ZODB Groups'
icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
manage_editForm=manage_editzodbGroupSourceForm
manage_tabs=Acquisition.Acquired
def __init__(self):
self.id='zodbGroupSource'
self.groups=PersistentMapping()
def addGroup(self, groupname, title='', users=(), **kw):
"""Creates a group"""
if self.groups.has_key(groupname):
raise ValueError, 'Group "%s" already exists' % groupname
a = 'before: groupname %s groups %s' % (groupname, self.groups)
group = apply(Group, (groupname,), kw)
group.setTitle(title)
group._setUsers(users)
self.groups[groupname] = group
def getGroup(self, groupname, default=_marker):
"""Returns the given group"""
try:
group = self.groups[groupname]
except KeyError:
if default is _marker: raise
return default
return group
def delGroup(self, groupname):
"""Deletes the given group"""
usernames = self.groups[groupname].getUsers()
#self.delUsersFromGroup(usernames, groupname)
del self.groups[groupname]
def listGroups(self):
"""Returns a list of group names"""
return tuple(self.groups.keys())
def getGroupsOfUser(self, username):
"Get a user's groups"
groupnames = []
allnames = self.listGroups()
groupnames = filter(lambda g, u=username, self=self: u in self.groups[g].getUsers(), allnames)
return tuple(groupnames)
def setGroupsOfUser(self, groupnames, username):
"Set a user's groups"
oldGroups = self.getGroupsOfUser(username)
self.delGroupsFromUser(oldGroups, username)
self.addGroupsToUser(groupnames, username)
def addGroupsToUser(self, groupnames, username):
"Add groups to a user"
for name in groupnames:
group = self.groups[name]
if not username in group.getUsers():
group._addUsers([username])
def delGroupsFromUser(self, groupnames, username):
"Delete groups from a user"
for name in groupnames:
group = self.groups[name]
if username in group.getUsers():
group._delUsers([username])
def getUsersOfGroup(self, groupname):
"Get the users in a group"
return self.groups[groupname].getUsers()
def setUsersOfGroup(self, usernames, groupname):
"Set the users in a group"
# uniquify
dict = {}
for u in usernames: dict[u] = None
usernames = dict.keys()
self.groups[groupname]._setUsers(usernames)
def addUsersToGroup(self, usernames, groupname):
"Add users to a group"
# uniquify
dict = {}
for u in usernames: dict[u] = None
usernames = dict.keys()
self.groups[groupname]._addUsers(usernames)
def delUsersFromGroup(self, usernames, groupname):
"Delete users from a group"
# uniquify
dict = {}
for u in usernames: dict[u] = None
usernames = dict.keys()
self.groups[groupname]._delUsers(usernames)
def deleteUsers(self, usernames):
"Delete a list of users"
for user in usernames:
groups = self.getGroupsOfUser(user)
self.delGroupsFromUser(groups, user)
def postInitialisation(self, REQUEST):
pass
def manage_beforeDelete(self, item, container):
# Notify the exUserFolder that it doesn't have a group source anymore
container.currentGroupSource=None
zodbGroupReg=PluginRegister('zodbGroupSource','Simplistic ZODB Group Source',
zodbGroupSource, manage_addzodbGroupSourceForm,
manage_addzodbGroupSource,
manage_editzodbGroupSourceForm)
exUserFolder.groupSources['zodbGroupSource']=zodbGroupReg

View File

@ -0,0 +1,91 @@
XUF as a whole is covered by the BSD License, however it uses software
covered by other compatible licenses (see below)
------------------------------------------------------------------------
All of the documentation and software included in the exUserFolder
Releases is copyrighted by The Internet (Aust) Pty Ltd and contributors
ACN: 082 081 472 ABN: 83 082 081 472
Copyright 2001, 2002 The Internet (Aust) Pty Ltd
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
------------------------------------------------------------------------
This product includes software developed by Digital Creations for use in
the Z Object Publishing Environment (http://www.zope.org/)
Portions of smbAuthSource Copyright (C) 2001 Michael Teo
Portions of radiusAuthSource Copyright (C) 1999 Stuart Bishop
fcrypt is Copyright (C) 2001, 2001 Carey Evans
This product includes cryptographic software written by Eric Young
(eay@mincom.oz.au)
------------------------------------------------------------------------
Brief discussion of what the license means to you, not meant to be
all encompassing, but, to give you the general idea. This editorial does
not need to be distributed d8)
If you want to incorporate this product (or parts of it) into a commercial
product that's fine.
If you want to modify this product that's fine.
If you want to modify and distribute this product that's fine (even in
commercial products).
If you want to incorporate this into a larger work that's fine (even
if that work has a different license).
None of the previous items place any obligation of notification, compensation,
or return of code to us. In fact we don't care if you do these things. Go
forth and prosper. Basically as long as you recognise that this doesn't
belong to you, you can do what you want with it even charge money for it.
Note: If you do distribute this as source, then the XUF components are
removable and distributable independently of your license as a whole
(although that's a lot of trouble to go to when they could just download it
from the same place you did).
What you can't do, is claim it's yours, and this one thing encompasses a lot
of things, here's a few.
If it's not yours you can't;
Change the license even if you change the code since the copyright
of the modified files remains with the original copyright holders.
Use bits of it inside products that require the license to change, because
only the copyright holders have the right to modify the license (not a
concern for commercial projects, only some other Free/Open Source licenses).
Assign the copyright or other IP to any other party of the whole or any
part (even if you change the code), because it's not yours to give away or
sell to a 3rd party.
If the fact you can almost do whatever you want with this code isn't
liberal enough for you, contact us and we'll see what we can arrange.

View File

@ -0,0 +1,27 @@
#
# Extensible User Folder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: LoginRequiredMessages.py,v 1.2 2001/12/01 08:40:03 akm Exp $
LoginRequiredMessages={
'session_expired':'Your Session has Expired',
'unauthorized':'Please Login',
'login_failed':'Login Failed',
}

View File

@ -0,0 +1,25 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2005 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id:
import basicMemberSource
import nullMemberSource

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,22 @@
<dtml-var "DialogHeader(DialogTitle='Change Password', dialog_width='')">
<form action="acl_users/manage_changePassword" method="POST">
<table>
<tr>
<td align="right"><b><dtml-babel src="'en'">Old Password</dtml-babel></b></td>
<td><input type="password" name="current_password"></td>
<tr>
<td align="right"><b><dtml-babel src="'en'">Password</dtml-babel></b></td>
<td><input type="password" name="password"></td>
</tr>
<td align="right"><b><dtml-babel src="'en'">Confirm Password</dtml-babel></b></td>
<td><input type="password" name="password_confirm"></td>
</tr>
<dtml-if "forgottenPasswords=='hint'">
<tr><td align="right"><b><dtml-babel src="'en'">Password Hint</dtml-babel></b></td>
<td><input type="text" name="user_hint" value="&dtml.missing-user_hint;"></td>
</tr>
</dtml-if>
</table>
<input type="submit" value=" <dtml-babel src="'en'">Change Password</dtml-babel> ">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,31 @@
<dtml-var "DialogHeader(DialogTitle='Signup', dialog_width='')">
<form action="acl_users/manage_signupUser" method="POST">
<table>
<tr>
<td align="right"><b><dtml-babel src="'en'">Username</dtml-babel></td>
<td><input name="username" type="text" value="&dtml.missing-username;"></td>
</tr>
<dtml-if "passwordPolicy=='user'">
<tr>
<td align="right"><b><dtml-babel src="'en'">Password</dtml-babel></b></td>
<td><input type="password" name="password" value="&dtml.missing-password;"></td>
</tr>
<td align="right"><b><dtml-babel src="'en'">Confirm Password</dtml-babel></b></td>
<td><input type="password" name="password_confirm"></td>
</tr>
<dtml-if "forgottenPasswords=='hint'">
<tr><td align="right"><b><dtml-babel src="'en'">Password Hint</dtml-babel></b></td>
<td><input type="text" name="user_hint" value="&dtml.missing-user_hint;"></td>
</tr>
</dtml-if>
</dtml-if>
<tr><td align="right"><b><dtml-babel src="'en'">Real Name</dtml-babel></b></td>
<td><input type="text" name="user_realname" value="&dtml.missing-user_realname;"></td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'"><dtml-babel src="'en'">Email</dtml-babel></dtml-babel></b></td>
<td><input type="text" name="user_email" value="&dtml.missing-user_email;"></td>
</tr>
</table>
<input type="submit" value=" <dtml-babel src="'en'">Signup</dtml-babel> ">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
import basicMemberSource

View File

@ -0,0 +1,629 @@
#
# Extensible User Folder
#
# Basic Membership Source for exUserFolder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: basicMemberSource.py,v 1.1 2004/11/10 14:15:55 akm Exp $
#
# Basically membership is a layer between the signup/login form, and
# the authentication layer, it uses the prop source of the users to
# store additional information about a user i.e. doesn't impact on the
# authentication source.
#
# Some membership features imply some extra properties for the user will
# be available; specifically at this time an email property.
#
# You also need a MailHost setup and ready to go for emailing stuff to users
#
import string,Acquisition
from random import choice
from Globals import HTMLFile, INSTANCE_HOME
from OFS.Folder import Folder
from OFS.DTMLMethod import DTMLMethod
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
from base64 import encodestring
from urllib import quote
import zLOG
"""
Password Policy enforcement (min/max length, caps etc)
Create Password, or User Chooses.
Timing out of passwords...
Empty Password force change on login...
Create Home Directory
Copy files from Skelton Directory
EMail password hint to user (forgot my password)
Reset password and email user (needs plugin?)
Redirect on login to fixed or varying per username location.
Automatically add users, or manually approve of users.
"""
# Stupid little things for making a password
# Don't hassle me, it's supposed to be basic.
nouns=['ace', 'ant', 'arc', 'arm', 'axe',
'bar', 'bat', 'bee', 'bib', 'bin',
'can', 'cap', 'car', 'cat', 'cob',
'day', 'den', 'dog', 'dot', 'dux',
'ear', 'eel', 'egg', 'elf', 'elk',
'fad', 'fan', 'fat', 'fig', 'fez',
'gag', 'gas', 'gin', 'git', 'gum',
'hag', 'hat', 'hay', 'hex', 'hub']
pastConjs = [ 'did', 'has', 'was' ]
suffixes = [ 'ing', 'es', 'ed', 'ious', 'ily']
def manage_addBasicMemberSource(self, REQUEST):
""" Add a Membership Source """
pvfeatures=[]
minLength=0
passwordPolicy=''
createHomedir=0
homeRoot=''
copyFilesFrom=''
postLogin=''
postSignup=''
forgottenPasswords=''
defaultRoles=[]
usersCanChangePasswords=0
baseURL=''
loginPage=''
signupPage=''
passwordPage=''
mailHost=''
fixedDest=''
if REQUEST.has_key('basicmember_pvfeatures'):
pvfeatures=REQUEST['basicmember_pvfeatures']
if REQUEST.has_key('basicmember_roles'):
defaultRoles=REQUEST['basicmember_roles']
if not defaultRoles:
defaultRoles=['Member']
if 'minlength' in pvfeatures:
minLength=REQUEST['basicmember_minpasslen']
if REQUEST.has_key('basicmember_passwordpolicy'):
passwordPolicy=REQUEST['basicmember_passwordpolicy']
if REQUEST.has_key('basicmember_createhomedir'):
homeRoot=REQUEST['basicmember_homeroot']
createHomedir=1
if REQUEST.has_key('basicmember_copyfiles'):
copyFilesFrom=REQUEST['basicmember_copyfiles']
if REQUEST.has_key('basicmember_changepasswords'):
usersCanChangePasswords=1
if REQUEST.has_key('basicmember_fixeddest'):
fixedDest=''
forgottenPasswords=REQUEST['basicmember_forgottenpasswords']
postLogin=REQUEST['basicmember_postlogin']
baseURL=REQUEST['basicmember_baseurl']
loginPage=REQUEST['basicmember_loginpage']
signupPage=REQUEST['basicmember_signuppage']
passwordPage=REQUEST['basicmember_passwordpage']
siteEmail=REQUEST['basicmember_siteemail']
siteName=REQUEST['basicmember_sitename']
mailHost=REQUEST['basicmember_mailhost']
# postSignup=REQUEST['basicmember_postsignup']
#
# Yep this is obscene
#
o = BasicMemberSource(pvfeatures, minLength, passwordPolicy,
createHomedir, copyFilesFrom, postLogin,
homeRoot, forgottenPasswords, defaultRoles,
usersCanChangePasswords, baseURL, loginPage,
signupPage, passwordPage, mailHost,
siteName, siteEmail, fixedDest)
self._setObject('basicMemberSource', o, None, None, 0)
o = getattr(self, 'basicMemberSource')
if hasattr(o, 'postInitialisation'):
o.postInitialisation(REQUEST)
self.currentMembershipSource=o
return ''
manage_addBasicMemberSourceForm=HTMLFile('manage_addBasicMemberSourceForm',
globals())
manage_editBasicMemberSourceForm=HTMLFile('manage_editBasicMemberSourceForm',
globals())
#
# Crap, I don't know why I called this basic, I'd hate to see a
# complicated one.
#
class BasicMemberSource(Folder):
""" Provide High Level User Management """
meta_type="Membership Source"
title="Basic Membership Source"
icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
manage_tabs=Acquisition.Acquired
manage_editForm=manage_editBasicMemberSourceForm
# Ugh...
def __init__(self, pvFeatures=[], minLength=0, passwordPolicy='',
createHomeDir=0, copyFilesFrom='', postLogin='', homeRoot='',
forgottenPasswords='', defaultRoles=[], usersCanChangePasswords=0,
baseURL='', loginPage='', signupPage='', passwordPage='',
mailHost='', siteName='', siteEmail='', fixedDest=''):
self.id='basicMemberSource'
self.pvFeatures=pvFeatures
self.minLength=int(minLength)
self.passwordPolicy=passwordPolicy
self.createHomeDir=createHomeDir
self.copyFilesFrom=copyFilesFrom
self.postLogin=postLogin
self.homeRoot=homeRoot
self.forgottenPasswords=forgottenPasswords
self.defaultRoles=defaultRoles
self.usersCanChangePasswords=usersCanChangePasswords
self.baseURL=baseURL
self.loginPage=loginPage
self.signupPage=signupPage
self.passwordPage=passwordPage
self.siteName=siteName
self.siteEmail=siteEmail
self.fixedDest=fixedDest
_SignupForm=HTMLFile('SignupForm', globals())
SignupForm=DTMLMethod()
SignupForm.manage_edit(data=_SignupForm, title='Signup Form')
self._setObject('SignupForm', SignupForm)
_PasswordForm=HTMLFile('PasswordForm', globals())
PasswordForm=DTMLMethod()
PasswordForm.manage_edit(data=_PasswordForm,
title='Change Password')
self._setObject('PasswordForm', PasswordForm)
self.mailHost=mailHost
_newPasswordEmail=HTMLFile('newPasswordEmail', globals())
newPasswordEmail=DTMLMethod()
newPasswordEmail.manage_edit(data=_newPasswordEmail,
title='Send New Password')
self._setObject('newPasswordEmail', newPasswordEmail)
_forgotPasswordEmail=HTMLFile('forgotPasswordEmail', globals())
forgotPasswordEmail=DTMLMethod()
forgotPasswordEmail.manage_edit(data=_forgotPasswordEmail,
title='Send Forgotten Password')
self._setObject('forgotPasswordEmail', forgotPasswordEmail)
_passwordHintEmail=HTMLFile('passwordHintEmail', globals())
passwordHintEmail=DTMLMethod()
passwordHintEmail.manage_edit(data=_passwordHintEmail,
title='Send Forgotten Password Hint')
self._setObject('passwordHintEmail', passwordHintEmail)
def postInitialisation(self, REQUEST):
if self.createHomeDir and self.homeRoot:
self.findHomeRootObject()
else:
self.homeRootObj=None
if self.copyFilesFrom:
self.findSkelRootObject()
else:
self.homeSkelObj=None
# The nice sendmail tag doesn't allow expressions for
# the mailhost
self.mailHostObject=getattr(self, self.mailHost)
def manage_editMembershipSource(self, REQUEST):
""" Edit a basic Membership Source """
if REQUEST.has_key('pvfeatures'):
self.pvFeatures=REQUEST['pvfeatures']
else:
self.pvFeatures=[]
if REQUEST.has_key('minpasslength'):
self.minLength=REQUEST['minpasslength']
if REQUEST.has_key('createhomedir'):
createHomeDir=1
else:
createHomeDir=0
if createHomeDir:
self.copyFilesFrom=REQUEST['copyfiles']
if self.copyFilesFrom:
self.findSkelRootObject()
else:
self.homeRoot=REQUEST['homeroot']
self.findHomeRootObject()
if REQUEST.has_key('memberroles'):
self.defaultRoles=REQUEST['memberroles']
if REQUEST.has_key('changepasswords'):
self.usersCanChangePasswords=1
else:
self.usersCanChangePasswords=0
self.postLogin=REQUEST['postlogin']
if REQUEST.has_key('fixeddest'):
self.fixedDest=REQUEST['fixeddest']
self.baseURL=REQUEST['baseurl']
self.loginPage=REQUEST['loginpage']
self.signupPage=REQUEST['signuppage']
self.passwordPage=REQUEST['passwordpage']
self.siteName=REQUEST['sitename']
self.siteEmail=REQUEST['siteemail']
return self.MessageDialog(self,
title ='Updated!',
message="Membership was Updated",
action ='manage_editMembershipSourceForm',
REQUEST=REQUEST)
def forgotPassword(self, REQUEST):
username=REQUEST['username']
curUser=self.getUser(username)
if not curUser:
return self.MessageDialog(self,
title ='No such user',
message="No users matching that username were found.",
action ='%s/%s'%(self.baseURL, self.loginPage),
REQUEST=REQUEST)
userEmail=curUser.getProperty('email')
userName=curUser.getProperty('realname')
if self.forgottenPasswords == "hint":
passwordHint=curUser.getProperty('passwordhint')
self.passwordHintEmail(self,
REQUEST=REQUEST,
username=username,
hint=passwordHint,
realname=userName,
email=userEmail)
else:
# make a new password, and mail it to the user
password = self.generatePassword()
curCrypt=self.currentAuthSource.cryptPassword(username,password)
# Update the user
bogusREQUEST={}
#bogusREQUEST['username']=username
bogusREQUEST['password']=password
bogusREQUEST['password_confirm']=password
bogusREQUEST['roles']=curUser.roles
self.manage_editUser(username, bogusREQUEST)
self.forgotPasswordEmail(self,
REQUEST=REQUEST,
username=username,
password=password,
realname=userName,
email=userEmail)
return self.MessageDialog(self,
title ='Sent!',
message="Password details have been emailed to you",
action ='%s/%s'%(self.baseURL, self.loginPage),
REQUEST=REQUEST)
def changeProperties(self, REQUEST):
curUser=self.listOneUser(REQUEST['AUTHENTICATED_USER'].getUserName())
curUser=curUser[0]
if not curUser:
return self.MessageDialog(self,
title ='Erm!',
message="You don't seem to be logged in",
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
self.currentPropSource.updateUser(curUser['username'],REQUEST)
return self.MessageDialog(self,
title ='Properties updated',
message="Your properties have been updated",
action =self.baseURL,
REQUEST=REQUEST,
)
def changePassword(self, REQUEST):
if not self.usersCanChangePasswords:
return ''
curUser=self.listOneUser(REQUEST['AUTHENTICATED_USER'].getUserName())
curUser=curUser[0]
if not curUser:
return self.MessageDialog(
title ='Erm!',
message="You don't seem to be logged in",
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
curCrypt=self.currentAuthSource.cryptPassword(curUser['username'],REQUEST['current_password'])
if curCrypt != curUser['password']:
return self.MessageDialog(self,
title ='Password Mismatch',
message="Password is incorrect",
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
if REQUEST['password'] != REQUEST['password_confirm']:
return self.MessageDialog(self,
title ='Password Mismatch',
message="Passwords do not match",
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
# OK the old password matches the one the user provided
# Both new passwords match...
# Time to validate against our normal set of rules...
#
if not self.validatePassword(REQUEST['password'], curUser['username']):
return self.MessageDialog(self,
title ='Password problem',
message="Your password is invalid, please choose another",
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
if self.passwordPolicy=='hint':
if not hasattr(REQUEST,'user_passwordhint'):
return self.MessageDialog(self,
title ='Password requires hint',
message='You must choose a password hint',
action ='%s/%s'%(self.baseURL, self.passwordPage),
REQUEST=REQUEST)
bogusREQUEST={}
bogusREQUEST['password']=REQUEST['password']
bogusREQUEST['password_confirm']=REQUEST['password']
bogusREQUEST['roles']=curUser['roles']
self.manage_editUser(curUser['username'],bogusREQUEST)
# update the cookie so he doesnt have to re-login:
if self.cookie_mode:
token='%s:%s' %(curUser['username'], REQUEST['password'])
token=encodestring(token)
token=quote(token)
REQUEST.response.setCookie('__ac', token, path='/')
REQUEST['__ac']=token
return self.MessageDialog(self,
title ='Password updated',
message="Your password has been updated",
action =self.baseURL,
REQUEST=REQUEST)
def goHome(self, REQUEST, RESPONSE):
redirectstring="%s/%s/%s/manage_main"%(self.baseURL, self.homeRoot, REQUEST.AUTHENTICATED_USER.getUserName())
RESPONSE.redirect(redirectstring)
return ''
# Tell exUserFolder where we want to go...
def getLoginDestination(self, REQUEST):
script=''
pathinfo=''
querystring=''
redirectstring=''
if self.postLogin=="destination":
script=REQUEST['SCRIPT_NAME']
pathinfo=REQUEST['PATH_INFO']
elif self.postLogin=="varied":
script=self.baseURL
pathinfo="/acl_users/goHome"
elif self.postLogin=="fixed":
pathinfo="%s"%(self.fixedDest)
if REQUEST.has_key('QUERY_STRING'):
querystring='?'+REQUEST['QUERY_STRING']
redirectstring=script+pathinfo
if querystring:
redirectstring=redirectstring+querystring
return redirectstring
def validatePassword(self, password, username):
if 'minlength' in self.pvFeatures:
if len(password) < self.minLength:
return 0
if 'mixedcase' in self.pvFeatures:
lower = 0
upper = 0
for c in password:
if c in string.lowercase:
lower = 1
if c in string.uppercase:
upper = 1
if not upper and lower:
return 0
if 'specialchar' in self.pvFeatures:
special = 0
for c in password:
if c in string.punctuation:
special = 1
break
elif c in string.digits:
special = 1
break
if not special:
return 0
#
# XXX Move this somewhere else
#
if 'notstupid' in self.pvFeatures:
email=''
# We try some permutations here...
curUser=self.getUser(username)
if curUser:
email = curUser.getProperty('email')
elif hasattr(self, 'REQUEST'):
if self.REQUEST.has_key('user_email'): # new signup
email=self.REQUEST['user_email']
elif self.REQUEST.has_key('email'):
email=self.REQUEST['email']
if ((string.find(password, username)>=0) or
( email and
(string.find(password,
string.split(email,'@')[0]) >=0))):
return 0
return 1
# These next two look the same (and they are for now), but, the reason I
# Don't use one single method, is I think that SkelObj might migrate to
# using full paths, not relative paths.
def findSkelRootObject(self):
# Parent should be acl_users
parent = getattr(self, 'aq_parent')
# This should be the root...
root = getattr(parent, 'aq_parent')
searchPaths = string.split(self.copyFilesFrom, '/')
for o in searchPaths:
if not getattr(root, o):
break
root = getattr(root, o)
self.homeSkelObj=root
def findHomeRootObject(self):
# Parent should be acl_users
parent = getattr(self, 'aq_parent')
# This should be the root...
root = getattr(parent, 'aq_parent')
searchPaths = string.split(self.homeRoot, '/')
for o in searchPaths:
if o not in root.objectIds():
root.manage_addFolder(id=o, title=o, createPublic=0, createUserF=0)
root = getattr(root, o)
self.homeRootObj=root
def makeHomeDir(self, username):
if not self.homeRootObj:
return
self.homeRootObj.manage_addFolder(id=username, title=username, createPublic=0, createUserF=0)
home = getattr(self.homeRootObj, username)
# Allow user to be in charge of their own destiny
# XXXX WARNING THIS IS A NORMAL FOLDER *SO USERS CAN ADD ANYTHING*
# YOU NEED TO CHANGE THE TYPE OF OBJECT ADDED FOR A USER UNLESS
# THIS IS WHAT YOU WANT TO HAPPEN
home.manage_addLocalRoles(userid=username, roles=['Manager'])
if self.copyFilesFrom and self.homeSkelObj and self.homeSkelObj.objectIds():
cp=self.homeSkelObj.manage_copyObjects(
self.homeSkelObj.objectIds())
home.manage_pasteObjects(cp)
# Fix it so the user owns their stuff
curUser=self.getUser(username).__of__(self.aq_parent)
home.changeOwnership(curUser, recursive=1)
def generatePassword(self):
password = (choice(nouns) + choice(pastConjs) +
choice(nouns) + choice(suffixes))
return password
def createUser(self, REQUEST):
if self.passwordPolicy == 'user':
if not self.validatePassword(REQUEST['password'], REQUEST['username']):
return self.MessageDialog(self,
title ='Password problem',
message='Your password is invalid, please choose another',
action ='%s/%s'%(self.baseURL, self.signupPage),
REQUEST=REQUEST)
if self.passwordPolicy=='hint':
if not hasattr(REQUEST,'user_passwordhint'):
return self.MessageDialog(self,
title ='Password requires hint',
message='You must choose a password hint',
action ='%s/%s'%(self.baseURL, self.signupPage),
REQUEST=REQUEST)
elif self.passwordPolicy == 'system':
REQUEST['password']=self.generatePassword()
REQUEST['password_confirm']=REQUEST['password']
# Email the password.
self.newPasswordEmail(self, REQUEST)
zLOG.LOG("exUserFolder.basicMemberSource", zLOG.BLATHER,
"Creating user",
"Passed all tests -- creating [%s]" % REQUEST['username'])
REQUEST['roles']=self.defaultRoles
self.manage_addUser(REQUEST) # Create the User...
if self.createHomeDir:
self.makeHomeDir(REQUEST['username'])
return self.MessageDialog(self,
title ='You have signed up',
message='You have been signed up succesfully',
action ='%s'%(self.baseURL),
REQUEST=REQUEST)
basicMemberReg=PluginRegister('basicMemberSource',
'Basic Membership Source',
BasicMemberSource,
manage_addBasicMemberSourceForm,
manage_addBasicMemberSource,
manage_editBasicMemberSourceForm)
exUserFolder.membershipSources['basicMemberSource']=basicMemberReg

View File

@ -0,0 +1,15 @@
<dtml-sendmail mailhost=mailHostObject>
To: <dtml-var realname> <<dtml-var email>>
From: <dtml-var siteName> <<dtml-var siteEmail>>
Subject: You forgot your password for <dtml-var siteName>
Dear <dtml-var realname>,
Your username is <dtml-var username> and your password is now
<dtml-var password>.
You should have tested this first, and now that you've tested it, you'll
see you need to customise this method.
</dtml-sendmail>

View File

@ -0,0 +1,143 @@
<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Membership Source', dialog_width='')">
<b><dtml-babel src="'en'">Membership requires a valid property source, you cannot use this with NULL Property Source</dtml-babel></b>
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
<dtml-let listVar=sequence-item>
<dtml-in "REQUEST[listVar]">
<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
</dtml-in>
</dtml-let>
<dtml-else>
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-if>
</dtml-in>
<input type="HIDDEN" name="doGroup" value="1">
<table cellspacing="2">
<tr><td align="right"><b><dtml-babel src="'en'">Site Name (used in emails)</dtml-babel></b></td>
<td><input type="text" name="basicmember_sitename">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Site Email (used for emails)</dtml-babel></b></td>
<td><input type="text" name="basicmember_siteemail">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Mail Host</dtml-babel></b></td>
<td>
<select name="basicmember_mailhost">
<dtml-in "MailHostIDs()">
<option value="<dtml-var sequence-item>">
<dtml-var sequence-key></option>
</dtml-in>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Site Base</dtml-babel></b></td>
<td><input type="text" name="basicmember_baseurl"
value="<dtml-var "absolute_url()">">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Login Page</dtml-babel></b></td>
<td><input type="text" name="basicmember_loginpage" value="LoginForm">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Signup Page</dtml-babel></b></td>
<td><input type="text" name="basicmember_signuppage" value="SignupForm">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Change Password Page</dtml-babel></b></td>
<td><input type="text" name="basicmember_passwordpage" value="ChangePasswordForm">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Password Validation Features</dtml-babel></b></td>
<td>
<select name="basicmember_pvfeatures:list" multiple>
<option value="minlength">Minimum Length</option>
<option value="mixedcase">Must have Mixed Case</option>
<option value="specichar">Must have Special Chars</option>
<option value="notstupid">Not Stupid (username/email/part of name)</option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Minimum Length (0 if not required)</dtml-babel></b></td>
<td>
<input type="text" name="basicmember_minpasslen:int" value="0">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Password Policy</dtml-babel></b></td>
<td>
<select name="basicmember_passwordpolicy">
<option value="user">User Chooses</option>
<option value="system">System Chooses and emails User</option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Forgotten Passwords</dtml-babel></b></td>
<td>
<select name="basicmember_forgottenpasswords">
<option value="hint"><dtml-babel src="'en'">Email a Hint</dtml-babel></option>
<option value="reset"><dtml-babel src="'en'">Reset and Email New password</dtml-babel></option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Allow users to change passwords</dtml-babel></b></td>
<td>
<input type="checkbox" name="basicmember_changepasswords" checked><dtml-babel src="'en'">Yes</dtml-babel>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Create 'Home Directory'</dtml-babel></b></td>
<td>
<input type="checkbox" name="basicmember_createhomedir"><dtml-babel src="'en'">Yes</dtml-babel>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path to 'Home Directory' Root</dtml-babel></b></td>
<td>
<input type="text" name="basicmember_homeroot" value="Members">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Copy initial 'Home Directory' files from...(empty=No Copy)</dtml-babel></b></td>
<td>
<input type="text" name="basicmember_copyfiles", value="<dtml-var "_.string.join(getPhysicalPath()[1:], '/')">">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">After login....</dtml-babel></b></td>
<td>
<select name="basicmember_postlogin">
<option value="destination"><dtml-babel src="'en'">Go to intended destination</dtml-babel></option>
<option value="fixed"><dtml-babel src="'en'">Go to fixed destination</dtml-babel></option>
<option value="varied"><dtml-babel src="'en'">Go to Home Directory</dtml-babel></option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Fixed Destination</dtml-babel></b></td>
<td>
<input type="text" name="basicmember_fixeddest">
</td>
</tr>
<tr>
<td valign="top" align="right"><b><dtml-babel src="'en'">Default Roles</dtml-babel></b></td>
<td align="left" valign="top">
<select name="basicmember_roles:list" size="5" multiple>
<dtml-in valid_roles>
<dtml-if expr="_vars['sequence-item'] != 'Anonymous'">
<dtml-if expr="_vars['sequence-item'] != 'Authenticated'">
<dtml-if expr="_vars['sequence-item'] != 'Shared'">
<option value="<dtml-var sequence-item html_quote>"><dtml-var sequence-item>
</dtml-if>
</dtml-if>
</dtml-if>
</dtml-in valid_roles>
</select>
</td>
</tr>
</table>
<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,116 @@
<dtml-var "DialogHeader(_.None, _, DialogTitle='Edit Basic Membership Source', dialog_width='')">
<dtml-var manage_tabs>
<FORM ACTION="manage_editMembershipSource" METHOD="POST">
<dtml-with currentMembershipSource>
<table cellspacing="2">
<tr><td align="right"><b><dtml-babel src="'en'">Site Name (used in emails)</dtml-babel></b></td>
<td><input type="text" name="sitename" value="&dtml.missing-siteName;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Site Email (used for emails)</dtml-babel></b></td>
<td><input type="text" name="siteemail" value="&dtml.missing-siteEmail;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Mail Host</dtml-babel></b></td>
<td>
<select name="mailhost">
<dtml-in "MailHostIDs()">
<option value="<dtml-var sequence-item>"<dtml-if "mailHost==_.getitem('sequence-item',0)"> selected</dtml-if>><dtml-var sequence-key></option>
</dtml-in>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Site Base</dtml-babel></b></td>
<td><input type="text" name="baseurl"
value="&dtml.missing-baseURL;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Login Page</dtml-babel></b></td>
<td><input type="text" name="loginpage" value="&dtml.missing-loginPage;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel>Relative Path (from base) of Signup Page</dtml-babel></b></td>
<td><input type="text" name="signuppage" value="&dtml.missing-signupPage;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Change Password Page</dtml-babel></b></td>
<td><input type="text" name="passwordpage" value="&dtml.missing-passwordPage;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Password Validation Features</dtml-babel></b></td>
<td>
<select name="pvfeatures:list" multiple>
<option value="minlength" <dtml-if "'minlength' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Minimum Length</dtml-babel></option>
<option value="mixedcase" <dtml-if "'mixedcase' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Must have Mixed Case</dtml-babel></option>
<option value="specichar" <dtml-if "'specichar' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Must have Special Chars</dtml-babel></option>
<option value="notstupid" <dtml-if "'notstupid' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Not Stupid (username/email/part of name)</dtml-babel></option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Minimum Length (if required)</dtml-babel></b></td>
<td>
<input type="text" name="minpasslen:int" value="&dtml.missing-minLength;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Allow users to change passwords</dtml-babel></b></td>
<td>
<input type="checkbox" name="changepasswords"<dtml-if usersCanChangePasswords> checked</dtml-if>><dtml-babel src="'en'">Yes</dtml-babel>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Create 'Home Directory'</dtml-babel></b></td>
<td>
<input type="checkbox" name="createhomedir"<dtml-if createHomeDir> checked</dtml-if>><dtml-babel src="'en'">Yes</dtml-babel>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Path to 'Home Directory' Root</dtml-babel></b></td>
<td>
<input type="text" name="homeroot" value="&dtml.missing-homeRoot;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Copy initial 'Home Directory' files from...(empty=No Copy)</dtml-babel></b></td>
<td>
<input type="text" name="copyfiles" value="&dtml.missing-copyFilesFrom;">
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">After login....</dtml-babel></b></td>
<td>
<select name="postlogin">
<option value="destination"<dtml-if "postLogin=='destination'"> selected</dtml-if>><dtml-babel src="'en'">Go to intended destination</dtml-babel></option>
<option value="fixed"<dtml-if "postLogin=='fixed'"> selected</dtml-if>><dtml-babel src="'en'">Go to fixed destination</dtml-babel></option>
<option value="varied"<dtml-if "postLogin=='varied'"> selected</dtml-if>><dtml-babel src="'en'">Go to Home Directory</dtml-babel></option>
</select>
</td>
</tr>
<tr><td align="right"><b><dtml-babel src="'en'">Fixed Destination</dtml-babel></b></td>
<td>
<input type="text" name="fixeddest" value="&dtml.missing-fixedDest;">
</td>
</tr>
<tr>
<td valign="top" align="right"><b><dtml-babel src="'en'">Default Roles</dtml-babel></b></td>
<td align="left" valign="top">
<select name="memberroles:list" size="5" multiple>
<dtml-in valid_roles>
<dtml-if expr="_vars['sequence-item'] != 'Anonymous'">
<dtml-if expr="_vars['sequence-item'] != 'Authenticated'">
<dtml-if expr="_vars['sequence-item'] != 'Shared'">
<option value="<dtml-var sequence-item html_quote>"<dtml-if "_['sequence-item'] in defaultRoles"> selected</dtml-if>><dtml-var sequence-item>
</dtml-if>
</dtml-if>
</dtml-if>
</dtml-in valid_roles>
</select>
</td>
</tr>
</table>
<input type="SUBMIT" value=" <dtml-babel src="'en'">Update</dtml-babel> ">
</dtml-with>
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,16 @@
<dtml-sendmail mailhost=mailHostObject>
To: <dtml-var user_realname> <<dtml-var user_email>>
From: <dtml-var siteName> <<dtml-var siteEmail>>
Subject: Welcome to <dtml-var siteName>
Dear <dtml-var user_realname>,
Welcome to <dtml-var siteName>.
Your username is <dtml-var username> and your password is <dtml-var password>.
You should have tested this first, and now that you've tested it, you'll
see you need to customise this method.
</dtml-sendmail>

View File

@ -0,0 +1,15 @@
<dtml-sendmail mailhost=mailHostObject>
To: <dtml-var realname> <<dtml-var email>>
From: <dtml-var siteName> <<dtml-var siteEmail>>
Subject: Hint for <dtml-var siteName>
Dear <dtml-var realname>,
Your username is <dtml-var username> and your password hint was;
<dtml-var hint>.
You should have tested this first, and now that you've tested it, you'll
see you need to customise this method.
</dtml-sendmail>

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
import nullMemberSource

View File

@ -0,0 +1,21 @@
<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Membership Source', dialog_width='')">
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
<dtml-let listVar=sequence-item>
<dtml-in "REQUEST[listVar]">
<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
</dtml-in>
</dtml-let>
<dtml-else>
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-if>
</dtml-in>
<input type="HIDDEN" name="doGroup" value="1">
<b><dtml-babel src="'en'">This Membership Source has no configuration Items</dtml-babel></b><br>
<br>
<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,49 @@
#
# Extensible User Folder
#
# Null Membership Source for exUserFolder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: nullMemberSource.py,v 1.1 2004/11/10 14:15:55 akm Exp $
from Globals import HTMLFile, INSTANCE_HOME
from OFS.Folder import Folder
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
from Products.exUserFolder.nullPlugin import nullPlugin
def manage_addNullMemberSource(self, REQUEST):
""" Add a Membership Source """
self.currentMembershipSource=None
return ''
manage_addNullMemberSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
manage_editNullMemberSourceForm=None
nullMemberReg=PluginRegister('nullMemberSource',
'Null Membership Source',
nullPlugin,
manage_addNullMemberSourceForm,
manage_addNullMemberSource,
manage_editNullMemberSourceForm)
exUserFolder.membershipSources['nullMemberSource']=nullMemberReg

View File

@ -0,0 +1,46 @@
#
#
# (C) Copyright 2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: Plugins.py,v 1.5 2004/11/10 14:15:33 akm Exp $
import App, Globals, OFS
import string
import time
from Globals import ImageFile, HTMLFile, HTML, MessageDialog, package_home
from OFS.Folder import Folder
class PluginRegister:
def __init__(self, name, description, pluginClass,
pluginStartForm, pluginStartMethod,
pluginEditForm=None, pluginEditMethod=None):
self.name=name #No Spaces please...
self.description=description
self.plugin=pluginClass
self.manage_addForm=pluginStartForm
self.manage_addMethod=pluginStartMethod
self.manage_editForm=pluginEditForm
self.manage_editMethod=pluginEditMethod
class CryptoPluginRegister:
def __init__(self, name, crypto, description, pluginMethod):
self.name = name #No Spaces please...
self.cryptoMethod = crypto
self.description = description
self.plugin = pluginMethod

View File

@ -0,0 +1,28 @@
#
# Extensible User Folder
#
# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
import nullPropSource
# aucune autre prop source pour ScoDoc

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,2 @@
# $Id: __init__.py,v 1.1 2004/11/10 14:15:56 akm Exp $
import nullPropSource

View File

@ -0,0 +1,21 @@
<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Property Source', dialog_width='')">
<FORM ACTION="&dtml-URL;" METHOD="POST">
<dtml-in "REQUEST.form.keys()">
<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
<dtml-let listVar=sequence-item>
<dtml-in "REQUEST[listVar]">
<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
</dtml-in>
</dtml-let>
<dtml-else>
<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
</dtml-if>
</dtml-in>
<input type="HIDDEN" name="doMember" value="1">
<b><dtml-babel src="'en'">This Property Source has no configuration Items</dtml-babel></b><br>
<br>
<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
</form>
<dtml-var DialogFooter>

View File

@ -0,0 +1,50 @@
#
# Extensible User Folder
#
# Null Membership Source for exUserFolder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: nullPropSource.py,v 1.1 2004/11/10 14:15:56 akm Exp $
from Globals import HTMLFile, INSTANCE_HOME
from OFS.Folder import Folder
from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister
from Products.exUserFolder.nullPlugin import nullPlugin
def manage_addNullPropSource(self, REQUEST):
""" Add a Property Source """
self.currentPropSource=None
return ''
manage_addNullPropSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
manage_editNullPropSourceForm=None
nullPropReg=PluginRegister('nullPropSource',
'Null Property Source',
nullPlugin,
manage_addNullPropSourceForm,
manage_addNullPropSource,
manage_editNullPropSourceForm)
exUserFolder.propSources['nullPropSource']=nullPropReg

View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
.*.swp

View File

@ -0,0 +1,122 @@
#
# Extensible User Folder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: PropertyEditor.py,v 1.3 2002/01/29 17:42:02 alex_zxc Exp $
from Globals import DTMLFile, MessageDialog, INSTANCE_HOME
from string import join,strip,split,lower,upper,find
from urllib import quote, unquote
def editStringProperty( name, value):
""" """
return('<input type="TEXT" name="%s:string" value="%s">\n'%(name, value))
def viewStringProperty( name, value):
""" """
return('<input type="HIDDEN" name="propValue:string" value="%s"><br>%s<br>\n'%(value, value))
def editIntegerProperty( name, value):
""" """
return('<input type="TEXT" name="%s:int" value="%d">\n'%(name, value or 0))
def viewIntegerProperty( name, value):
""" """
return('<input type="HIDDEN" name="propValue:int" value="%d"><br>%d<br>\n'%(value or 0 , value or 0))
def editLongProperty( name, value):
""" """
return('<input type="TEXT" name="%s:int" value="%d">\n'%(name, value or 0))
def viewLongProperty( name, value):
""" """
return('<input type="HIDDEN" name="propValue:long" value="%d"><br>%d<br>\n'%(value or 0, value or 0))
def editFloatProperty( name, value):
""" """
return('<input type="TEXT" name="%s:float" value="%d">\n'%(name, value))
def viewFloatProperty( name, value):
""" """
return('<input type="HIDDEN" name="propValue:float" value="%f"><br>%f<br>\n'%(value, value))
def editListProperty( name, value):
a=''
if value:
a = a + 'Select Items to keep<br>\n'
a = a + '<select name="%s:list" multiple>\n'%(name)
for i in value:
a = a + (
'<option value="%s" SELECTED>%s\n'%(i, i))
a = a + '</select>\n<br>'
a = a + 'Add an item\n<br>'
a = a + '<input type="TEXT" name="%s:list">'%(name)
return(a)
def viewListProperty( name, value):
a=''
if value:
for i in value:
a = a + (
'<input type="HIDDEN" name="propValue:list" value="%s">\n'%(i))
a = a + '%s\n<br>'%(i)
return(a)
def editDictProperty( name, value):
""" """
a=''
if value and value.keys():
for i in value.keys():
a = a + '%s : <input type="TEXT" name="%s.%s" value="%s">\n<br>'%(i, name, i, value[i])
return a
def viewDictProperty( name, value):
""" """
a=''
if value and value.keys():
for i in value.keys():
a = a + '%s : <input type="HIDDEN" name="propValue.%s" value="%s">\n<br>'%(i, name, i, value[i])
a = a + '%s\n<br>'%(value[i])
return a
EditMethods={'String':editStringProperty,
'Integer':editIntegerProperty,
'Long':editLongProperty,
'Float':editFloatProperty,
'List':editListProperty,
'Dict':editDictProperty}
ViewMethods={'String':viewStringProperty,
'Integer':viewIntegerProperty,
'Long':viewLongProperty,
'Float':viewFloatProperty,
'List':viewListProperty,
'Dict':viewDictProperty}

View File

@ -0,0 +1,22 @@
#
# Extensible User Folder
#
# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
# ACN: 082 081 472 ABN: 83 082 081 472
# All Rights Reserved
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Author: Andrew Milton <akm@theinternet.com.au>
# $Id: __init__.py,v 1.2 2001/12/01 08:40:04 akm Exp $
import PropertyEditor

View File

@ -0,0 +1,14 @@
If you are upgrading an existing site from < 0.50
I have restructured the source tree. This will make this version
incompatible with previous versions, as the classes have moved. This
breaks upgrading existing installs unless you keep the old classes
around. If you only use external Auth/Prop/Group sources, you will
probably be unaffected.
This means for those of you using SQL or LDAP or any non-ZODB sources,
you can remove and then re-add your XUF acl_users to get going again.
If you are using a ZODB source, then you need to keep the old classes
and the old paths around (e.g. symlink zodbAuthSource to
AuthSources/zodbAuthSource).

Some files were not shown because too many files have changed in this diff Show More