Compare commits

..

No commits in common. "master" and "nosex" have entirely different histories.

1897 changed files with 159568 additions and 374733 deletions

View File

@ -1,20 +0,0 @@
# Fichier à configurer et renommer en .env
# (dans /opt/scodoc)
# Il doit appartenir à (ou être lisible par) "scodoc"
FLASK_APP=scodoc.py
FLASK_ENV=production # ou "development" si vous développez
# Envois de mails: adapter si nécessaire
# MAIL_SERVER=localhost
# Obligatoire:
SCODOC_ADMIN_MAIL="emmanuel@viennet.net"
# Remplacer cette chaine
# Vous pouvez utiliser
# python3 -c "import uuid; print(uuid.uuid4().hex)"
# pour en créer une de ce genre, aléatoire
SECRET_KEY="53ffeff44a3940dea4964d628af33dd9"

View File

@ -1,3 +0,0 @@
[flake8]
max-line-length = 88
ignore = E203,W503

10
.gitignore vendored
View File

@ -131,7 +131,6 @@ venv/
ENV/
env.bak/
venv.bak/
envsco8/
# Spyder project settings
.spyderproject
@ -169,13 +168,4 @@ Thumbs.db
.vscode/
*.code-workspace
# PyCharm
.idea/
copy
# Symlinks static ScoDoc
app/static/links/[0-9]*.*[0-9]
# Essais locaux
xp/

View File

@ -1,2 +0,0 @@
[mypy-flask_login.*]
ignore_missing_imports = True

View File

@ -1,24 +0,0 @@
[MASTER]
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=pylint_flask
[TYPECHECK]
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=Permission,
SQLObject,
Registrant,
scoped_session,
func
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=entreprises
good-names=d,df,e,f,i,j,k,n,nt,t,u,ue,v,x,y,z,H,F

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2021 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
@ -25,50 +25,46 @@
#
##############################################################################
""" Importation des étudiants à partir de fichiers CSV
""" Importation des etudiants à partir de fichiers CSV
"""
import collections
import io
import os
import re
import sys
import time
import pdb
import collections
import types
import re
from flask import g, url_for
from app import db, log
from app.models import Identite, GroupDescr, ScolarNews
from app.models.etudiants import input_civilite, input_civilite_etat_civil
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_exceptions import (
ScoFormatError,
import sco_utils as scu
import notesdb as ndb
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
from sco_exceptions import (
AccessDenied,
FormatError,
ScoException,
ScoValueError,
ScoInvalidDateError,
ScoLockedFormError,
ScoGenError,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
import app.scodoc.notesdb as ndb
from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules,
)
import app.scodoc.sco_utils as scu
# format description (in tools/)
FORMAT_FILE = "format_import_etudiants.txt"
# 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",
"prenom_etat_civil",
"civilite_etat_civil",
"date_naissance",
"lieu_naissance",
"bac",
@ -100,6 +96,8 @@ ADMISSION_MODIFIABLE_FIELDS = (
"paysdomicile",
"telephone",
"telephonemobile",
# Debouche
"debouche",
# Groupes
"groupes",
)
@ -110,7 +108,7 @@ ADMISSION_MODIFIABLE_FIELDS = (
def sco_import_format(with_codesemestre=True):
"returns tuples (Attribut, Type, Table, AllowNulls, Description)"
r = []
for l in open(os.path.join(scu.SCO_TOOLS_DIR, FORMAT_FILE)):
for l in open(scu.SCO_SRCDIR + "/" + FORMAT_FILE):
l = l.strip()
if l and l[0] != "#":
fs = l.split(";")
@ -152,9 +150,11 @@ def sco_import_generate_excel_sample(
with_codesemestre=True,
only_tables=None,
with_groups=True,
exclude_cols=(),
extra_cols=(),
group_ids=(),
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())
@ -162,158 +162,160 @@ def sco_import_generate_excel_sample(
(only columns from these tables will be generated)
If group_ids, liste les etudiants de ces groupes
"""
style = sco_excel.excel_make_style(bold=True)
style_required = sco_excel.excel_make_style(bold=True, color=COLORS.RED)
style = sco_excel.Excel_MakeStyle(bold=True)
style_required = sco_excel.Excel_MakeStyle(bold=True, color="red")
titles = []
titles_styles = []
titlesStyles = []
for l in fmt:
name = l[0].lower()
name = scu.strlower(l[0])
if (not with_codesemestre) and name == "codesemestre":
continue # pas de colonne codesemestre
if only_tables is not None and l[2].lower() not in only_tables:
if only_tables is not None and scu.strlower(l[2]) not in only_tables:
continue # table non demandée
if name in exclude_cols:
continue # colonne exclue
if int(l[3]):
titles_styles.append(style)
titlesStyles.append(style)
else:
titles_styles.append(style_required)
titlesStyles.append(style_required)
titles.append(name)
if with_groups and "groupes" not in titles:
titles.append("groupes")
titles_styles.append(style)
titlesStyles.append(style)
titles += extra_cols
titles_styles += [style] * len(extra_cols)
if group_ids:
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
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
titles_styles = [style] + titles_styles
titlesStyles = [style] + titlesStyles
# rempli table avec données actuelles
lines = []
for i in members:
etud = sco_etud.get_etud_info(etudid=i["etudid"], filled=True)[0]
etud = context.getEtudInfo(etudid=i["etudid"], filled=True)[0]
l = []
for field in titles:
if field == "groupes":
sco_groups.etud_add_group_infos(
etud,
groups_infos.formsemestre_id,
sep=";",
with_default_partition=False,
context, etud, groups_infos.formsemestre, sep=";"
)
l.append(etud["partitionsgroupes"])
else:
key = field.lower().split()[0]
key = scu.strlower(field).split()[0]
l.append(etud.get(key, ""))
lines.append(l)
else:
lines = [[]] # empty content, titles only
return sco_excel.excel_simple_table(
titles=titles, titles_styles=titles_styles, sheet_name="Étudiants", lines=lines
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,
return_html=True,
):
"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 return_html:
if REQUEST:
if formsemestre_id:
dest = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
dest = "formsemestre_status?formsemestre_id=%s" % formsemestre_id
else:
dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
H = [html_sco_header.sco_header(page_title="Import etudiants")]
dest = context.NotesURL()
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) + html_sco_header.sco_footer()
return "\n".join(H) + context.sco_footer(REQUEST)
def scolars_import_excel_file(
datafile: io.BytesIO,
datafile,
context,
REQUEST,
formsemestre_id=None,
check_homonyms=True,
require_ine=False,
exclude_cols=(),
exclude_cols=[],
):
"""Importe etudiants depuis fichier Excel
et les inscrit dans le semestre indiqué (et à TOUS ses modules)
"""
log(f"scolars_import_excel_file: {formsemestre_id}")
cnx = ndb.GetDBConnexion()
log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id)
cnx = context.GetDBConnexion(autocommit=False)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
annee_courante = time.localtime()[0]
always_require_ine = sco_preferences.get_preference("always_require_ine")
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_bytes_to_list(exceldata)
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 = l[0].lower().split()[0] # titles in lowercase, and take 1st word
tit = scu.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 = [scu.stripquotes(s).lower().split()[0] for s in data[0]]
except Exception as exc:
raise ScoValueError("Titres de colonnes invalides (ou vides ?)") from exc
fs = [scu.strlower(scu.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(list(titles.keys()))
missing = {}.fromkeys(titles.keys())
unknown = []
for f in fs:
if f in missing:
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), list(missing.keys()), unknown)
"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 t not in titles:
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 = []
np_imported_homonyms = 0
NbImportedHomonyms = 0
GroupIdInferers = {}
try: # --- begin DB transaction
linenum = 0
@ -323,18 +325,20 @@ def scolars_import_excel_file(
values = {}
fs = line
# remove quotes
for i, field in enumerate(fs):
if field and (
(field[0] == '"' and field[-1] == '"')
or (field[0] == "'" and field[-1] == "'")
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] = field[1:-1]
for i, field in enumerate(fs):
val = field.strip()
typ, table, allow_nulls, descr, aliases = tuple(titles[titleslist[i]])
if not val and not allow_nulls:
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(
f"line {linenum}: null value not allowed in column {titleslist[i]}"
"line %d: null value not allowed in column %s"
% (linenum, titleslist[i])
)
if val == "":
val = None
@ -343,11 +347,11 @@ def scolars_import_excel_file(
val = val.replace(",", ".") # si virgule a la française
try:
val = float(val)
except (ValueError, TypeError) as exc:
except:
raise ScoValueError(
f"""valeur nombre reel invalide ({
val}) sur ligne {linenum}, colonne {titleslist[i]}"""
) from exc
"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"
@ -356,49 +360,34 @@ def scolars_import_excel_file(
if val % 1.0 > 1e-4:
raise ValueError()
val = int(val)
except (ValueError, TypeError) as exc:
except:
raise ScoValueError(
f"""valeur nombre entier invalide ({
val}) sur ligne {linenum}, colonne {titleslist[i]}"""
) from exc
# Ad-hoc checks (should be in format description)
if titleslist[i].lower() == "civilite":
"valeur nombre entier invalide (%s) sur ligne %d, colonne %s"
% (val, linenum, titleslist[i])
)
# xxx Ad-hoc checks (should be in format description)
if scu.strlower(titleslist[i]) == "sexe":
try:
val = input_civilite(val)
except ScoValueError as exc:
val = scolars.input_civilite(val)
except:
raise ScoValueError(
f"""valeur invalide pour 'civilite'
(doit etre 'M', 'F', ou 'MME', 'H', 'X' mais pas '{
val}') ligne {linenum}, colonne {titleslist[i]}"""
) from exc
if titleslist[i].lower() == "civilite_etat_civil":
try:
val = input_civilite_etat_civil(val)
except ScoValueError as exc:
raise ScoValueError(
f"""valeur invalide pour 'civilite'
(doit etre 'M', 'F', vide ou 'MME', 'H', 'X' mais pas '{
val}') ligne {linenum}, colonne {titleslist[i]}"""
) from exc
"valeur invalide pour 'SEXE' (doit etre 'M', 'F', ou 'MME', 'H', 'X' ou vide, mais pas '%s') ligne %d, colonne %s"
% (val, linenum, titleslist[i])
)
# Excel date conversion:
if titleslist[i].lower() == "date_naissance":
if scu.strlower(titleslist[i]) == "date_naissance":
if val:
try:
val = sco_excel.xldate_as_datetime(val)
except ValueError as exc:
raise ScoValueError(
f"""date invalide ({val}) sur ligne {
linenum}, colonne {titleslist[i]}"""
) from exc
if re.match(r"^[0-9]*\.?[0-9]*$", str(val)):
val = sco_excel.xldate_as_datetime(float(val))
# INE
if (
titleslist[i].lower() == "code_ine"
scu.strlower(titleslist[i]) == "code_ine"
and always_require_ine
and not val
):
raise ScoValueError(
"Code INE manquant sur ligne {linenum}, colonne {titleslist[i]}"
"Code INE manquant sur ligne %d, colonne %s"
% (linenum, titleslist[i])
)
# --
@ -413,34 +402,36 @@ def scolars_import_excel_file(
if values["code_ine"] and not is_new_ine:
raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
# Check nom/prenom
ok = False
homonyms = []
if "nom" in values and "prenom" in values:
ok, homonyms = sco_etud.check_nom_prenom_homonyms(
nom=values["nom"], prenom=values["prenom"]
)
ok, NbHomonyms = scolars.check_nom_prenom(
cnx, nom=values["nom"], prenom=values["prenom"]
)
if not ok:
raise ScoValueError(
f"nom ou prénom invalide sur la ligne {linenum}"
"nom ou prénom invalide sur la ligne %d" % (linenum)
)
if homonyms:
np_imported_homonyms += 1
if NbHomonyms:
NbImportedHomonyms += 1
# Insert in DB tables
_import_one_student(
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
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(f"scolars_import_excel_file: detected {np_imported_homonyms} homonyms")
if check_homonyms and np_imported_homonyms > len(created_etudids) / 10:
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(
f"Il y a trop d'homonymes ({np_imported_homonyms} étudiants)"
"Il y a trop d'homonymes (%d étudiants)" % NbImportedHomonyms
)
except:
cnx.rollback()
@ -449,7 +440,7 @@ def scolars_import_excel_file(
# here we try to remove all created students
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
for etudid in created_etudids:
log(f"scolars_import_excel_file: deleting etudid={etudid}")
log("scolars_import_excel_file: deleting etudid=%s" % etudid)
cursor.execute(
"delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
{"etudid": etudid},
@ -464,12 +455,15 @@ def scolars_import_excel_file(
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 id=%(etudid)s", {"etudid": etudid}
"delete from identite where etudid=%(etudid)s", {"etudid": etudid}
)
cnx.commit()
log("scolars_import_excel_file: re-raising exception")
@ -477,116 +471,86 @@ def scolars_import_excel_file(
diag.append("Import et inscription de %s étudiants" % len(created_etudids))
ScolarNews.add(
typ=ScolarNews.NEWS_INSCR,
sco_news.add(
context,
REQUEST,
typ=NEWS_INSCR,
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
% len(created_etudids),
obj=formsemestre_id,
max_frequency=0,
object=formsemestre_id,
)
log("scolars_import_excel_file: completing transaction")
cnx.commit()
# Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
for formsemestre_id in formsemestre_to_invalidate:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
context.Notes._inval_cache(formsemestre_id_list=formsemestre_to_invalidate)
return diag
def students_import_admission(
csvfile, type_admission="", formsemestre_id=None, return_html=True
):
"import donnees admission from Excel file (v2016)"
diag = scolars_import_admission(
csvfile,
formsemestre_id=formsemestre_id,
type_admission=type_admission,
)
if return_html:
H = [html_sco_header.sco_header(page_title="Import données admissions")]
H.append("<p>Import terminé !</p>")
H.append(
f"""<p><a class="stdlink" href="{ url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
}">Continuer</a></p>"""
)
if diag:
H.append(
f"""<p>Diagnostic: <ul><li>{
"</li><li>".join(diag)
}</li></ul></p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
def _import_one_student(
context,
cnx,
REQUEST,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
) -> int:
):
"""
Import d'un étudiant et inscription dans le semestre.
Return: id du semestre dans lequel il a été inscrit.
"""
log(f"scolars_import_excel_file: formsemestre_id={formsemestre_id} values={values}")
log(
"scolars_import_excel_file: formsemestre_id=%s values=%s"
% (formsemestre_id, str(values))
)
# Identite
args = values.copy()
args["annee"] = annee_courante
etud: Identite = Identite.create_from_dict(args)
etud.admission.from_dict(args)
etudid = etud.id
etudid = scolars.identite_create(cnx, args, context=context, REQUEST=REQUEST)
created_etudids.append(etudid)
# Admissions
args["etudid"] = etudid
args["annee"] = annee_courante
_ = scolars.admission_create(cnx, args)
# Adresse
args["typeadresse"] = "domicile"
args["description"] = "(infos admission)"
adresse = etud.adresses.first()
adresse.from_dict(args)
db.session.add(etud)
db.session.commit()
_ = scolars.adresse_create(cnx, args)
# Inscription au semestre
args["etat"] = scu.INSCRIT # etat insc. 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"]
try:
formsemestre_id = int(formsemestre_id)
except (ValueError, TypeError) as exc:
raise ScoValueError(
f"valeur invalide ou manquante dans la colonne codesemestre, ligne {linenum+1}"
) from exc
# recupere liste des groupes:
if formsemestre_id not in GroupIdInferers:
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id)
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 = list({}.fromkeys(group_ids).keys()) # uniq
group_ids = {}.fromkeys(group_ids).keys() # uniq
if None in group_ids:
raise ScoValueError(
f"groupe invalide sur la ligne {linenum} (groupe {groupes})"
"groupe invalide sur la ligne %d (groupe %s)" % (linenum, groupes)
)
do_formsemestre_inscription_with_modules(
int(args["formsemestre_id"]),
context,
args["formsemestre_id"],
etudid,
group_ids,
etat=scu.INSCRIT,
etat="I",
REQUEST=REQUEST,
method="import_csv_file",
)
return args["formsemestre_id"]
@ -594,12 +558,14 @@ def _import_one_student(
def _is_new_ine(cnx, code_ine):
"True if this code is not in DB"
etuds = sco_etud.identite_list(cnx, {"code_ine": code_ine})
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, formsemestre_id=None, type_admission=None):
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
@ -610,20 +576,18 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
é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".
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.
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(f"scolars_import_admission: formsemestre_id={formsemestre_id}")
log("scolars_import_admission: formsemestre_id=%s" % formsemestre_id)
members = sco_groups.get_group_members(
sco_groups.get_default_group(formsemestre_id)
context, sco_groups.get_default_group(context, formsemestre_id)
)
etuds_by_nomprenom = {} # { nomprenom : etud }
diag = []
@ -636,32 +600,29 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
etuds_by_nomprenom[np] = m
exceldata = datafile.read()
diag2, data = sco_excel.excel_bytes_to_list(exceldata)
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 = ndb.GetDBConnexion()
cnx = context.GetDBConnexion()
titles = data[0]
# idx -> ('field', convertor)
fields = adm_get_fields(titles, formsemestre_id)
idx_nom = None
idx_prenom = None
for idx, field in fields.items():
if field[0] == "nom":
for idx in fields:
if fields[idx][0] == "nom":
idx_nom = idx
if field[0] == "prenom":
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 ScoFormatError(
raise FormatError(
"scolars_import_admission: colonnes nom et prenom requises",
dest_url=url_for(
"scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
% formsemestre_id,
)
modifiable_fields = set(ADMISSION_MODIFIABLE_FIELDS)
@ -672,29 +633,27 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
# 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 (nom, prenom) not in etuds_by_nomprenom:
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
diag.append(msg)
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 = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0]
cur_adm = scolars.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
# peuple les champs presents dans le tableau
args = {}
for idx, field in fields.items():
field_name, convertor = field
for idx in fields:
field_name, convertor = fields[idx]
if field_name in modifiable_fields:
try:
val = convertor(line[idx])
except ValueError as exc:
raise ScoFormatError(
f"""scolars_import_admission: valeur invalide, ligne {
nline} colonne {field_name}: '{line[idx]}'""",
dest_url=url_for(
"scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
) from exc
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:
@ -703,57 +662,49 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
# Type admission: traitement particulier
if not cur_adm["type_admission"] and not args.get("type_admission"):
args["type_admission"] = type_admission
sco_etud.etudident_edit(cnx, args, disable_notify=True)
adr = sco_etud.adresse_list(cnx, args={"etudid": etud["etudid"]})
scolars.etudident_edit(cnx, args)
adr = scolars.adresse_list(cnx, args={"etudid": etud["etudid"]})
if adr:
args["adresse_id"] = adr[0]["adresse_id"]
sco_etud.adresse_edit(
cnx, args, disable_notify=True
) # pas de notification ici
scolars.adresse_edit(
cnx, args
) # ne passe pas le contexte: pas de notification ici
else:
args["typeadresse"] = "domicile"
args["description"] = "(infos admission)"
adresse_id = sco_etud.adresse_create(cnx, args)
adresse_id = scolars.adresse_create(cnx, args)
# log('import_adm: %s' % args )
# Change les groupes si nécessaire:
if "groupes" in args:
gi = sco_groups.GroupIdInferer(formsemestre_id)
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 = list({}.fromkeys(group_ids).keys()) # uniq
group_ids = {}.fromkeys(group_ids).keys() # uniq
if None in group_ids:
raise ScoValueError(
f"groupe invalide sur la ligne {nline} (groupes {groupes})"
"groupe invalide sur la ligne %d (groupe %s)"
% (nline, groupes)
)
for group_id in group_ids:
group = db.session.get(GroupDescr, group_id)
if group.partition.groups_editable:
sco_groups.change_etud_group_in_partition(
args["etudid"], group
)
else:
log("scolars_import_admission: partition non editable")
diag.append(
f"Attention: partition {group.partition} non editable (ignorée)"
)
sco_groups.change_etud_group_in_partition(
context, args["etudid"], group_id, REQUEST=REQUEST
)
#
diag.append(f"import de {etud['nomprenom']}")
diag.append("import de %s" % (etud["nomprenom"]))
n_import += 1
nline += 1
diag.append(f"{n_import} lignes importées")
diag.append("%d lignes importées" % n_import)
if n_import > 0:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
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 scu.suppress_accents(_ADM_PATTERN.sub("", s.strip().lower())).replace(
def adm_normalize_string(s): # normalize unicode title
return scu.suppression_diacritics(_ADM_PATTERN.sub("", s.strip().lower())).replace(
"_", ""
)
@ -762,15 +713,16 @@ def adm_get_fields(titles, formsemestre_id):
"""Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
return: { idx : (field_name, convertor) }
"""
format_dict = sco_import_format_dict()
# 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 format_dict:
for v in format_dict[k]["aliases"]:
for k in Fmt:
for v in Fmt[k]["aliases"]:
if adm_normalize_string(v) == title_n:
typ = format_dict[k]["type"]
typ = Fmt[k]["type"]
if typ == "real":
convertor = adm_convert_real
elif typ == "integer" or typ == "int":
@ -779,13 +731,11 @@ def adm_get_fields(titles, formsemestre_id):
convertor = adm_convert_text
# doublons ?
if k in [x[0] for x in fields.values()]:
raise ScoFormatError(
f"""scolars_import_admission: titre "{title}" en double (ligne 1)""",
dest_url=url_for(
"scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
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
@ -794,24 +744,24 @@ def adm_get_fields(titles, formsemestre_id):
def adm_convert_text(v):
if isinstance(v, float):
if type(v) == types.FloatType:
return "{:g}".format(v) # evite "1.0"
return v
def adm_convert_int(v):
if type(v) != int and not v:
if type(v) != types.IntType and not v:
return None
return int(float(v)) # accept "10.0"
def adm_convert_real(v):
if type(v) != float and not v:
if type(v) != types.FloatType and not v:
return None
return float(v)
def adm_table_description_format():
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:
@ -836,9 +786,9 @@ def adm_table_description_format():
tab = GenTable(
titles=titles,
columns_ids=columns_ids,
rows=list(Fmt.values()),
rows=Fmt.values(),
html_sortable=True,
html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(),
preferences=context.get_preferences(),
)
return tab

178
README.md
View File

@ -1,186 +1,20 @@
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
# SCODOC - gestion de la scolarité
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
Installation: voir instructions à jour sur <https://scodoc.org>
Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9
Ce logiciel est un produit pour Zope 2.13 écrit en Python (2.4, passé à 2.7 pour ScoDoc7).
La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
3.9+**.
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (dec 22)
- 9.4.x est en production
- le prochain jalon est 9.5. Voir branches sur gitea.
### Lignes de commandes
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
## Organisation des fichiers
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
les fichiers locaux (archives, photos, configurations, logs) sous
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données
postgresql et la configuration du système Linux.
### Fichiers locaux
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
`/opt/scodoc-data/config`.
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
Principaux contenus:
/opt/scodoc-data
/opt/scodoc-data/log # Fichiers de log ScoDoc
/opt/scodoc-data/config # Fichiers de configuration
.../config/logos # Logos de l'établissement
.../config/depts # un fichier par département
/opt/scodoc-data/photos # Photos des étudiants
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
## Pour les développeurs
### Installation du code
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
Puis remplacer `/opt/scodoc` par un clone du git.
sudo su
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
apt-get install git # si besoin
cd /opt
git clone https://scodoc.org/git/viennet/ScoDoc.git
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
# Renommer le répertoire:
mv ScoDoc scodoc
# Et donner ce répertoire à l'utilisateur scodoc:
chown -R scodoc.scodoc /opt/scodoc
Il faut ensuite installer l'environnement et le fichier de configuration:
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
mv /opt/off-scodoc/venv /opt/scodoc
Et la config:
ln -s /opt/scodoc-data/.env /opt/scodoc
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
### Tests unitaires
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
Avant le premier lancement, créer cette base ainsi:
./tools/create_database.sh SCODOC_TEST
export FLASK_ENV=test
flask db upgrade
Cette commande n'est nécessaire que la première fois (le contenu de la base
est effacé au début de chaque test, mais son schéma reste) et aussi si des
migrations (changements de schéma) ont eu lieu dans le code.
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
scripts de tests:
Lancer au préalable:
flask delete-dept -fy TEST00 && flask create-dept TEST00
Puis dérouler les tests unitaires:
pytest tests/unit
Ou avec couverture (`pip install pytest-cov`)
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
#### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base de données de
développement dans un état connu, par exemple pour éviter de recréer à la main
étudiants et semestres quand on développe.
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
par les tests:
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple:
pytest tests/unit/test_sco_basic.py
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
utilisateur:
flask user-password admin
**Attention:** les tests unitaires **effacent** complètement le contenu de la
base de données (tous les départements, et les utilisateurs) avant de commencer !
#### Modification du schéma de la base
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
flask db migrate -m "message explicatif....."
flask db upgrade
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !).
dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL
flask db upgrade # créé les tables à partir des migrations
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
# puis imports:
flask import-scodoc7-users
flask import-scodoc7-dept STID SCOSTID
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape.
### Profiling
Sur une machine de DEV, lancer
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
pip install snakeviz
puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
## Paquet Debian 12
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou
upgrade de scodoc9).
La préparation d'une release se fait à l'aide du script
`tools/build_release.sh`.

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

View File

@ -6,46 +6,31 @@
E. Viennet 2005 - 2008
v 1.3 (python3)
v 1.2
"""
import html
import re
import flask_wtf
import wtforms
from app import log
from app.scodoc.sco_exceptions import ScoInvalidCSRF
import app.scodoc.sco_utils as scu
# re validant dd/mm/yyyy
DMY_REGEXP = re.compile(
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
)
from types import BooleanType, StringType
def TrivialFormulator(
form_url,
values,
formdescription=(),
initvalues=None,
initvalues={},
method="post",
enctype=None,
submitlabel="OK",
name=None,
formid="tf",
form_attrs="",
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=None,
submitbuttonattributes=[],
top_buttons=False, # place buttons at top of form
bottom_buttons=True, # buttons after form
html_foot_markup="",
readonly=False,
is_submitted=False,
title="",
after_table="",
before_table="{title}",
):
"""
form_url : URL for this form
@ -67,7 +52,7 @@ def TrivialFormulator(
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: convert int and float values (from string)
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)
@ -82,10 +67,8 @@ def TrivialFormulator(
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'table_separator',
'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest',
'color'
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
'text_suggest'
(default text)
size : text field width
rows, cols: textarea geometry
@ -104,25 +87,21 @@ def TrivialFormulator(
form_url,
values,
formdescription,
initvalues or {},
initvalues,
method,
enctype,
submitlabel,
name,
formid,
form_attrs=form_attrs,
cssclass=cssclass,
cssclass,
cancelbutton=cancelbutton,
submitbutton=submitbutton,
submitbuttonattributes=submitbuttonattributes or [],
submitbuttonattributes=submitbuttonattributes,
top_buttons=top_buttons,
bottom_buttons=bottom_buttons,
html_foot_markup=html_foot_markup,
readonly=readonly,
is_submitted=is_submitted,
title=title,
after_table=after_table,
before_table=before_table,
)
form = t.getform()
if t.canceled():
@ -134,36 +113,32 @@ def TrivialFormulator(
return res, form, t.result
class TF(object):
class TF:
def __init__(
self,
form_url,
values,
formdescription=None,
initvalues=None,
formdescription=[],
initvalues={},
method="POST",
enctype=None,
submitlabel="OK",
name=None,
formid="tf",
form_attrs="",
cssclass="",
cancelbutton=None,
submitbutton=True,
submitbuttonattributes=None,
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,
title="",
after_table="",
before_table="{title}",
):
self.form_url = form_url
self.values = values.copy()
self.formdescription = list(formdescription or [])
self.initvalues = initvalues or {}
self.values = values
self.formdescription = list(formdescription)
self.initvalues = initvalues
self.method = method
self.enctype = enctype
self.submitlabel = submitlabel
@ -172,17 +147,13 @@ class TF(object):
else:
self.name = formid # 'tf'
self.formid = formid
self.form_attrs = form_attrs
self.cssclass = cssclass
self.cancelbutton = cancelbutton
self.submitbutton = submitbutton
self.submitbuttonattributes = submitbuttonattributes or []
self.submitbuttonattributes = submitbuttonattributes
self.top_buttons = top_buttons
self.bottom_buttons = bottom_buttons
self.html_foot_markup = html_foot_markup
self.title = title
self.after_table = after_table
self.before_table = before_table
self.readonly = readonly
self.result = None
self.is_submitted = is_submitted
@ -194,26 +165,11 @@ class TF(object):
"true if form has been submitted"
if self.is_submitted:
return True
form_submitted = self.values.get(f"{self.formid}_submitted", False)
if form_submitted:
self.check_csrf()
return form_submitted
def check_csrf(self):
"""check token for POST forms.
Raises ScoInvalidCSRF on failure.
"""
if self.method == "post":
token = self.values.get("csrf_token")
try:
flask_wtf.csrf.validate_csrf(token)
except wtforms.validators.ValidationError as exc:
log(f"Form.check_csrf: invalid CSRF token\n{exc.args}")
raise ScoInvalidCSRF() from exc
return self.values.get("%s-submitted" % self.formid, False)
def canceled(self):
"true if form has been canceled"
return self.values.get(f"{self.formid}_cancel", False)
return self.values.get("%s_cancel" % self.formid, False)
def getform(self):
"return HTML form"
@ -237,36 +193,28 @@ class TF(object):
def setdefaultvalues(self):
"set default values and convert numbers to strings"
for field, descr in self.formdescription:
for (field, descr) in self.formdescription:
# special case for boolcheckbox
if descr.get("input_type", None) == "boolcheckbox" and self.submitted():
if field not in self.values:
if not self.values.has_key(field):
self.values[field] = 0
else:
self.values[field] = 1
if field not in self.values:
if (descr.get("input_type", None) == "checkbox") and self.submitted():
# aucune case cochée
self.values[field] = []
else:
if "default" in descr: # 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] is None:
self.values[field] = ""
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, except ids
if field.endswith("id") and self.values[field]:
# enforce integer ids:
try:
self.values[field] = int(self.values[field])
except ValueError:
pass
elif isinstance(self.values[field], (int, float)):
# 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 "tf-checked" not in self.values:
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"] = []
@ -278,7 +226,7 @@ class TF(object):
"check values. Store .result and returns msg"
ok = 1
msg = []
for field, descr in self.formdescription:
for (field, descr) in self.formdescription:
val = self.values[field]
# do not check "unckecked" items
if descr.get("withcheckbox", False):
@ -287,22 +235,20 @@ class TF(object):
# null values
allow_null = descr.get("allow_null", True)
if not allow_null:
if val is None or (isinstance(val, str) and not val.strip()):
if val == "" or val == None:
msg.append(
"Le champ '%s' doit être renseigné" % descr.get("title", field)
)
ok = 0
elif val == "" or val == None:
continue # allowed empty field, skip
# type
typ = descr.get("type", "string")
if val != "" and val is not None:
if val != "" and val != None:
# check only non-null values
if typ[:3] == "int":
try:
val = int(val)
self.values[field] = val
except ValueError:
except:
msg.append(
"La valeur du champ '%s' doit être un nombre entier" % field
)
@ -312,53 +258,28 @@ class TF(object):
try:
val = float(val.replace(",", ".")) # allow ,
self.values[field] = val
except ValueError:
except:
msg.append(
"La valeur du champ '%s' doit être un nombre" % field
)
ok = 0
if (
ok
and (typ[:3] == "int" or typ == "float" or typ == "real")
and val != ""
and val != None
):
if "min_value" in descr and self.values[field] < descr["min_value"]:
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 "max_value" in descr and self.values[field] > descr["max_value"]:
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
if typ[:3] == "int":
if not (scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT):
msg.append(
f"Le champ '{field}' est a une valeur hors limite"
)
ok = 0
elif typ == "float" or typ == "real":
if not (
scu.DB_MIN_FLOAT <= self.values[field] <= scu.DB_MAX_FLOAT
):
msg.append(
f"Le champ '{field}' est a une valeur hors limite"
)
ok = 0
if ok and (typ[:3] == "str") and "max_length" in descr:
if len(self.values[field]) > descr["max_length"]:
msg.append(
"Le champ '%s' est trop long (max %d caractères)"
% (field, descr["max_length"])
)
ok = 0
# allowed values
if "allowed_values" in descr:
if descr.has_key("allowed_values"):
if descr.get("input_type", None) == "checkbox":
# for checkboxes, val is a list
for v in val:
@ -372,41 +293,22 @@ class TF(object):
elif not val in descr["allowed_values"]:
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
if "validator" in descr:
try:
valid = descr["validator"](val, field)
except Exception:
valid = False
if not valid:
if descr.has_key("validator"):
if not descr["validator"](val, field):
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
elif descr.get("input_type") == "datedmy":
if not DMY_REGEXP.match(val):
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
ok = 0
# boolean checkbox
if descr.get("input_type", None) == "boolcheckbox":
if int(val):
self.values[field] = True
self.values[field] = 1
else:
self.values[field] = False
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":
try:
self.values[field] = int(self.values[field])
except ValueError:
msg.append(
f"valeur invalide ({self.values[field]}) pour le champ {field}"
)
ok = False
self.values[field] = int(self.values[field])
elif typ == "float" or typ == "real":
try:
self.values[field] = float(self.values[field].replace(",", "."))
except ValueError:
msg.append(
f"valeur invalide ({self.values[field]}) pour le champ {field}"
)
ok = False
self.values[field] = float(self.values[field].replace(",", "."))
if ok:
self.result = self.values
else:
@ -439,7 +341,7 @@ class TF(object):
)
if self.cancelbutton:
buttons_markup += (
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
% (self.formid, self.formid, self.cancelbutton)
)
@ -456,29 +358,16 @@ class TF(object):
klass = ""
name = self.name
R.append(
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s %s>'
% (
self.form_url,
self.method,
self.formid,
enctype,
name,
klass,
self.form_attrs,
)
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
% (self.form_url, self.method, self.formid, enctype, name, klass)
)
if self.method == "post":
R.append(
f"""<input type="hidden" name="csrf_token" value="{
flask_wtf.csrf.generate_csrf()
}"/>"""
)
R.append(f"""<input type="hidden" name="{self.formid}_submitted" value="1"/>""")
R.append('<input type="hidden" name="%s-submitted" value="1"/>' % self.formid)
if self.top_buttons:
R.append(buttons_markup + "<p></p>")
R.append(self.before_table.format(title=self.title))
R.append('<table class="tf">')
for field, descr in self.formdescription:
idx = 0
for idx in range(len(self.formdescription)):
(field, descr) = self.formdescription[idx]
if descr.get("readonly", False):
R.append(self._ReadOnlyElement(field, descr))
continue
@ -492,7 +381,7 @@ class TF(object):
input_type = descr.get("input_type", "text")
item_dom_id = descr.get("dom_id", "")
if item_dom_id:
item_dom_attr = f' id="{item_dom_id}"'
item_dom_attr = ' id="%s"' % item_dom_id
else:
item_dom_attr = ""
# choix du template
@ -504,16 +393,6 @@ class TF(object):
etempl = separatortemplate
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
continue
elif input_type == "table_separator":
etempl = ""
# Table ouverte ?
if len([p for p in R if "<table" in p]) > len(
[p for p in R if "</table" in p]
):
R.append(f"""</table>{self.after_table}""")
R.append(
f"""{self.before_table.format(title=descr.get("title", ""))}<table class="tf">"""
)
else:
etempl = itemtemplate
lab = []
@ -557,13 +436,13 @@ class TF(object):
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)
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)
lem.append(('value="%(' + field + ')s" />') % values)
elif input_type == "radio":
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
@ -584,14 +463,14 @@ class TF(object):
elif input_type == "menu":
lem.append('<select name="%s" id="%s" %s>' % (field, wid, attribs))
labels = descr.get("labels", descr["allowed_values"])
allowed_values = list(descr["allowed_values"])
for i, label in enumerate(labels):
if str(allowed_values[i]) == str(values[field]):
for i in range(len(labels)):
if str(descr["allowed_values"][i]) == str(values[field]):
selected = "selected"
else:
selected = ""
lem.append(
f"""<option value="{allowed_values[i]}" {selected}>{label}</option>"""
'<option value="%s" %s>%s</option>'
% (descr["allowed_values"][i], selected, labels[i])
)
lem.append("</select>")
elif input_type == "checkbox" or input_type == "boolcheckbox":
@ -604,25 +483,21 @@ class TF(object):
disabled_items = descr.get("disabled_items", {})
if vertical:
lem.append("<table>")
for i in range(len(labels)): # pylint: disable=consider-using-enumerate
for i in range(len(labels)):
if input_type == "checkbox":
if (
values[field]
and descr["allowed_values"][i] in values[field]
):
# 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
if values[field] == "True":
v = True
elif values[field] == "False":
v = False
else:
try:
v = int(values[field])
except (ValueError, KeyError):
v = False
# 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:
@ -671,25 +546,18 @@ class TF(object):
'<input type="hidden" name="%s" id="%s" value="%s" %s />'
% (field, wid, values[field], attribs)
)
elif (input_type == "separator") or (input_type == "table_separator"):
elif input_type == "separator":
pass
elif input_type == "file":
lem.append(
'<input type="file" name="%s" size="%s" value="%s" %s>'
'<input type="file" name="%s" size="%s" value="%s" %s/>'
% (field, size, values[field], attribs)
)
elif (
input_type == "date" or input_type == "datedmy"
): # JavaScript widget for date input
elif input_type == "date": # JavaScript widget for date input
lem.append(
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
'<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
% (field, values[field])
)
elif input_type == "time": # JavaScript widget for date input
lem.append(
f"""<input type="text" name="{field}" maxlength="5" size="5" value="{
values[field]}" class="timepicker">"""
)
elif input_type == "text_suggest":
lem.append(
'<input type="text" name="%s" id="%s" size="%d" %s'
@ -697,25 +565,25 @@ class TF(object):
)
lem.append(('value="%(' + field + ')s" />') % values)
suggest_js.append(
f"""var {field}_opts = {dict2js(descr.get("text_suggest_options", {}))};
var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
"""
"""var %s_opts = %s;
var %s_as = new bsn.AutoSuggest('%s', %s_opts);
"""
% (
field,
dict2js(descr.get("text_suggest_options", {})),
field,
field,
field,
)
)
elif input_type == "color":
lem.append(
'<input type="color" name="%s" id="%s" %s' % (field, field, attribs)
)
lem.append(('value="%(' + field + ')s" >') % values)
else:
raise ValueError(f"unkown input_type for form ({input_type})!")
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
if explanation:
lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
lem.append('<span class="tf-explanation">%s</span>' % explanation)
comment = descr.get("comment", "")
if comment:
if (input_type != "checkbox") and (input_type != "boolcheckbox"):
lem.append("<br>")
lem.append(f"""<span class="tf-comment">{comment}</span>""")
lem.append('<br/><span class="tf-comment">%s</span>' % comment)
R.append(
etempl
% {
@ -725,11 +593,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
}
)
R.append("</table>")
R.append(self.after_table)
R.append(self.html_foot_markup)
if self.bottom_buttons:
R.append("<br>" + buttons_markup)
R.append("<br/>" + buttons_markup)
if add_no_enter_js:
R.append(
@ -751,7 +619,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
break;
i = i + 1;
while (i < elem.form.elements.length) {
if ((elem.form.elements[i].type == "text")
if ((elem.form.elements[i].type == "text")
&& (!(elem.form.elements[i].disabled))
&& ($(elem.form.elements[i]).is(':visible')))
{
@ -765,7 +633,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
elem.blur();
}
return false;
}
}
else
return true;
}</script>
@ -776,7 +644,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
# => only one form with text_suggest field on a page.
R.append(
"""<script type="text/javascript">
function init_tf_form(formid) {
function init_tf_form(formid) {
%s
}
</script>"""
@ -821,49 +689,35 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
if input_type == "separator": # separator
R.append('<td colspan="2">%s' % title)
elif input_type != "table_separator":
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 in ("text", "text_suggest", "color", "datedmy"):
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", ["non", "oui"])
"labels", descr.get("allowed_values", ["oui", "non"])
)
_val = self.values[field]
if isinstance(_val, bool):
bool_val = 1 if _val else 0
elif _val == "False":
bool_val = 0
elif _val:
bool_val = 1
else:
bool_val = 0
R.append(labels[bool_val])
if bool_val:
R.append(f'<input type="hidden" name="{field}" value="1"/>')
# 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>' % html.escape(self.values[field])
)
elif (
input_type == "separator"
or input_type == "hidden"
or input_type == "table_separator"
):
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(f"unkown input_type for form ({input_type})!")
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
if explanation:
@ -876,7 +730,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
def _ReadOnlyVersion(self, formdescription):
"Generate HTML for read-only view of the form"
R = ['<table class="tf-ro">']
for field, descr in formdescription:
for (field, descr) in formdescription:
R.append(self._ReadOnlyElement(field, descr))
R.append("</table>")
return R
@ -887,12 +741,12 @@ def dict2js(d):
r = []
for k in d:
v = d[k]
if isinstance(v, bool):
if type(v) == BooleanType:
if v:
v = "true"
else:
v = "false"
elif isinstance(v, str): # ne marchera pas en python2
elif type(v) == StringType:
v = '"' + v + '"'
r.append("%s: %s" % (k, v))
@ -903,9 +757,9 @@ def tf_error_message(msg):
"""html for form error message"""
if not msg:
return ""
if isinstance(msg, str):
if type(msg) == StringType:
msg = [msg]
return (
'<ul class="tf-msg"><li class="tf-msg error-message">%s</li></ul>'
% '</li><li class="tf-msg tf-msg error-message">'.join(msg)
'<ul class="tf-msg"><li class="tf-msg">%s</li></ul>'
% '</li><li class="tf-msg">'.join(msg)
)

View File

@ -1,72 +1,21 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.939"
SCOVERSION = "7.23"
SCONAME = "ScoDoc"
SCONEWS = """
<h4>Année 2023</h4>
<ul>
<li>ScoDoc 9.6 (juillet 2023)</li>
<ul>
<li>Nouveaux bulletins BUT compacts</li>
<li>Nouvelle gestion des absences et assiduité</li>
<li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li>
</ul>
<li>ScoDoc 9.5 (juillet 2023)</li>
<ul>
<li>Version de maintenance (sécurité et correctifs critiques) sur Debian 11: fin de vie: 1/11/2023</li>
</ul>
<li>ScoDoc 9.4</li>
<ul>
<li>Connexion avec service CAS</li>
<li>Améliorations des tableaux récapitulatifs</li>
<li>Nouvelle interface de gestions des groupes (S. Lehmann)</li>
<li>Enrichissement des jurys BUT et des procès-verbaux associés.</li>
</ul>
</ul>
<h4>Année 2022</h4>
<ul>
<li>ScoDoc 9.4</li>
<ul>
<li>Jury BUT2 avec parcours BUT</li>
</ul>
<li>ScoDoc 9.3</li>
<ul>
<li>Nouvelle API REST pour connecter ScoDoc à d'autres applications</li>
<li>Module de gestion des relations avec les entreprises</li>
<li>Prise en charge des parcours BUT</li>
<li>Association des UEs aux compétences du référentiel</li>
<li>Jury BUT1</li>
</ul>
<h4>Année 2021</h4>
<ul>
<li>ScoDoc 9.2:
<ul>
<li>Tableau récap. complet pour BUT et autres formations.</li>
<li>Tableau état évaluations</li>
<li>Export des trombinoscope en document docx</li>
<li>Très nombreux correctifs</li>
</ul>
<li>ScoDoc 9.1.75: bulletins BUT pdf</li>
<li>ScoDoc 9.1.50: nombreuses amélioration gestion BUT</li>
<li>ScoDoc 9.1: gestion des formations par compétences, type BUT.</li>
<li>ScoDoc 9.0: nouvelle architecture logicielle (Flask/Python3/Debian 11)</li>
<li>Version mobile (en test)</li>
<li>Évaluations de type "deuxième session"</li>
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li>
<li>Diverses corrections (PV de jurys, ...)</li>
<li>Modernisation du code Python</li>
</ul>
<h4>Année 2020</h4>
<ul>
<li>Corrections d'erreurs, améliorations saisie absences et affichage bulletins</li>
<li>Nouveau site <a href="https://scodoc.org" target="_blank" rel="noopener noreferrer">scodoc.org</a> pour la documentation</li>
<li>Corrections d'erreurs, améliorations saise absences< et affichage bulletins</li>
<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
@ -176,6 +125,7 @@ SCONEWS = """
<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>

1923
ZAbsences.py Normal file

File diff suppressed because it is too large Load Diff

1301
ZEntreprises.py Normal file

File diff suppressed because it is too large Load Diff

3308
ZNotes.py Normal file

File diff suppressed because it is too large Load Diff

976
ZScoDoc.py Normal file
View File

@ -0,0 +1,976 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 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
import datetime
import string
import glob
import re
import inspect
import urllib
import urllib2
import cgi
import xml
from cStringIO import StringIO
from zipfile import ZipFile
import os.path
import traceback
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
MIMEMultipart,
)
from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error
from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error
from email.Header import Header # pylint: disable=no-name-in-module,import-error
from email import Encoders # pylint: disable=no-name-in-module,import-error
from sco_zope import * # pylint: disable=unused-wildcard-import
try:
import Products.ZPsycopgDA.DA as ZopeDA
except:
import ZPsycopgDA.DA as ZopeDA # interp.py
import sco_utils as scu
import VERSION
from notes_log import log
import sco_find_etud
import sco_users
from sco_permissions import (
ScoView,
ScoEnsView,
ScoImplement,
ScoChangeFormation,
ScoObservateur,
ScoEtudInscrit,
ScoEtudChangeGroups,
ScoEtudChangeAdr,
ScoEtudSupprAnnotations,
ScoEditAllEvals,
ScoEditAllNotes,
ScoEditFormationTags,
ScoEditApo,
ScoSuperAdmin,
)
from sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError, AccessDenied
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
+ ({"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:
_ = 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:
_ = self.getAuthFailedMessage
except:
log("adding getAuthFailedMessage to Zope install")
parent = self.aq_parent
from OFS.DTMLMethod import addDTMLMethod # pylint: disable=import-error
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",
scu.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 not sco_users.is_valid_password(password):
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:
_ = 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
log("creating Zope folder " + DeptId)
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
log("creating Zope folder %s/Fotos" % DeptId)
add_method = DeptFolder.manage_addProduct["OFSP"].manage_addFolder
add_method("Fotos", title="Photos identites " + DeptId)
# 3- Creation instance ScoDoc
log("creating Zope ZScolar instance")
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="/ScoDoc">Continuer</a></p>"""
)
_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": scu.SCO_ENCODING},
self._top_level_css,
"""</head><body class="gtrcontent" id="gtrcontent">""",
scu.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":
return 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>"""
% (scu.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""" % (
scu.SCO_ENCODING,
scu.CUSTOM_HTML_HEADER_CNX,
)
security.declareProtected("View", "standard_html_footer")
def standard_html_footer(self, REQUEST=None):
"""Le pied de page HTML de la page d'accueil."""
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>""" % (
scu.SCO_USERS_LIST,
scu.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, # unused ?
error_type=None,
error_traceback=None,
error_tb=None,
**kv
):
"Recuperation des exceptions Zope"
# 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":
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())
params = {
"error_type": error_type,
"error_value": error_value,
"error_tb": error_tb,
"sco_exc_mail": scu.SCO_EXC_MAIL,
"sco_dev_mail": scu.SCO_DEV_MAIL,
}
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> """
% params
)
# display error traceback (? may open a security risk via xss attack ?)
# log('exc B')
params["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>
"""
% params
)
try:
H.append(self.standard_html_footer(REQUEST))
except:
log("no footer found for error page")
pass
# --- Mail:
params["error_traceback_txt"] = scu.scodoc_html2txt(error_tb)
txt = (
"""
ErrorType: %(error_type)s
%(error_traceback_txt)s
"""
% params
)
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"""
QUERY_STRING = REQUEST.get("QUERY_STRING", "")
if QUERY_STRING:
QUERY_STRING = "?" + QUERY_STRING
if fmt == "txt":
REFERER = REQUEST.get("HTTP_REFERER", "")
HTTP_USER_AGENT = REQUEST.get("HTTP_USER_AGENT", "")
else:
REFERER = "na"
HTTP_USER_AGENT = "na"
params = dict(
AUTHENTICATED_USER=REQUEST.get("AUTHENTICATED_USER", ""),
dt=time.asctime(),
URL=REQUEST.get("URL", ""),
QUERY_STRING=QUERY_STRING,
METHOD=REQUEST.get("REQUEST_METHOD", ""),
REFERER=REFERER,
HTTP_USER_AGENT=HTTP_USER_AGENT,
form=REQUEST.get("form", ""),
HTTP_X_FORWARDED_FOR=REQUEST.get("HTTP_X_FORWARDED_FOR", ""),
svn_version=scu.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
"""
% params
)
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 scu.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, scu.SCO_ENCODING)
msg["Subject"] = subj
recipients = [scu.SCO_EXC_MAIL]
msg["To"] = " ,".join(recipients)
msg["From"] = "scodoc-alert"
msg.epilogue = ""
msg.attach(MIMEText(txt, "plain", scu.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(scu.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("/ScoDoc/manage_workspace")
return id

1322
ZScoUsers.py Normal file

File diff suppressed because it is too large Load Diff

2775
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.

Before

Width:  |  Height:  |  Size: 1.4 KiB

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

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