Compare commits
No commits in common. "master" and "nosex" have entirely different histories.
20
.env-exemple
@ -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"
|
||||
|
10
.gitignore
vendored
@ -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/
|
||||
|
24
.pylintrc
@ -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
|
||||
|
@ -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
@ -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
@ -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]), #  
|
||||
(u"!", [0xA1]), # ¡
|
||||
(u"c", [0xA2]), # cent
|
||||
(u"L", [0xA3]), # pound
|
||||
(u"o", [0xA4]), # currency symbol
|
||||
(u"Y", [0xA5]), # yen
|
||||
(u"|", [0xA6]), # Broken Bar ¦
|
||||
(u"S", [0xA7]), # section
|
||||
(u"", [0xA8]), # diaeresis ¨
|
||||
(u"", [0xA9]), # copyright
|
||||
(u'"', [0xAB, 0xBA]), # «, » <<, >>
|
||||
(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]), # ·,
|
||||
(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
@ -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
|
||||
|
@ -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)
|
||||
)
|
@ -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
1301
ZEntreprises.py
Normal file
976
ZScoDoc.py
Normal 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è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
2775
ZScolar.py
Normal file
5
ZopeProducts/README
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
Produits Zope2 anciens et adaptes pour ScoDoc
|
||||
|
||||
E. Viennet 2013
|
||||
|
372
ZopeProducts/ZPsycopgDA/DA.py
Normal 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'
|
||||
}
|
29
ZopeProducts/ZPsycopgDA/__init__.py
Normal 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')
|
209
ZopeProducts/ZPsycopgDA/db.py
Normal 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
|
108
ZopeProducts/ZPsycopgDA/dtml/add.dtml
Normal 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>
|
11
ZopeProducts/ZPsycopgDA/dtml/browse.dtml
Normal 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>
|
84
ZopeProducts/ZPsycopgDA/dtml/edit.dtml
Normal 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>
|
7
ZopeProducts/ZPsycopgDA/dtml/table_info.dtml
Normal 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>
|
BIN
app/static/icons/favicon.ico → ZopeProducts/ZPsycopgDA/icons/DBAdapterFolder_icon.gif
Normal file → Executable file
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 897 B |
BIN
ZopeProducts/ZPsycopgDA/icons/bin.gif
Normal file
After Width: | Height: | Size: 924 B |
BIN
ZopeProducts/ZPsycopgDA/icons/date.gif
Normal file
After Width: | Height: | Size: 930 B |
BIN
ZopeProducts/ZPsycopgDA/icons/datetime.gif
Normal file
After Width: | Height: | Size: 925 B |
BIN
ZopeProducts/ZPsycopgDA/icons/field.gif
Normal file
After Width: | Height: | Size: 915 B |
BIN
ZopeProducts/ZPsycopgDA/icons/float.gif
Normal file
After Width: | Height: | Size: 929 B |
BIN
ZopeProducts/ZPsycopgDA/icons/int.gif
Normal file
After Width: | Height: | Size: 918 B |
BIN
ZopeProducts/ZPsycopgDA/icons/stable.gif
Normal file
After Width: | Height: | Size: 884 B |
BIN
ZopeProducts/ZPsycopgDA/icons/table.gif
Normal file
After Width: | Height: | Size: 878 B |
BIN
ZopeProducts/ZPsycopgDA/icons/text.gif
Normal file
After Width: | Height: | Size: 918 B |
BIN
ZopeProducts/ZPsycopgDA/icons/time.gif
Normal file
After Width: | Height: | Size: 926 B |
BIN
ZopeProducts/ZPsycopgDA/icons/view.gif
Normal file
After Width: | Height: | Size: 893 B |
BIN
ZopeProducts/ZPsycopgDA/icons/what.gif
Normal file
After Width: | Height: | Size: 894 B |
193
ZopeProducts/ZPsycopgDA/pool.py
Normal 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)
|
47
ZopeProducts/exUserFolder/AuthSources/__init__.py
Normal 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
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:36 akm Exp $
|
||||
import pgAuthSource
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
"""
|
865
ZopeProducts/exUserFolder/CHANGES.txt
Normal 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..
|
4
ZopeProducts/exUserFolder/CryptoSources/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
import pass_crypt
|
||||
import pass_md5
|
||||
import pass_sha
|
||||
import pass_plain
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
35
ZopeProducts/exUserFolder/CryptoSources/fcrypt/ChangeLog
Normal 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.
|
77
ZopeProducts/exUserFolder/CryptoSources/fcrypt/LICENSE
Normal 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.]
|
@ -0,0 +1 @@
|
||||
include LICENSE ChangeLog MANIFEST.in
|
13
ZopeProducts/exUserFolder/CryptoSources/fcrypt/PKG-INFO
Normal 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
|
33
ZopeProducts/exUserFolder/CryptoSources/fcrypt/README
Normal 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
|
@ -0,0 +1 @@
|
||||
import fcrypt
|
602
ZopeProducts/exUserFolder/CryptoSources/fcrypt/fcrypt.py
Normal 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()
|
22
ZopeProducts/exUserFolder/CryptoSources/fcrypt/setup.py
Normal 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'] )
|
44
ZopeProducts/exUserFolder/CryptoSources/pass_crypt.py
Normal 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
|
47
ZopeProducts/exUserFolder/CryptoSources/pass_md5.py
Normal 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
|
31
ZopeProducts/exUserFolder/CryptoSources/pass_plain.py
Normal 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
|
41
ZopeProducts/exUserFolder/CryptoSources/pass_sha.py
Normal 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
|
4
ZopeProducts/exUserFolder/Extensions/.cvsignore
Normal file
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
20
ZopeProducts/exUserFolder/Extensions/getOldGroups.py
Normal 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
|
26
ZopeProducts/exUserFolder/Extensions/loadOldGroups.py
Normal 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
|
||||
|
140
ZopeProducts/exUserFolder/Extensions/usAuthSourceMethods.py
Normal 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>
|
||||
"""
|
||||
|
4
ZopeProducts/exUserFolder/GroupSource/.cvsignore
Normal file
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
32
ZopeProducts/exUserFolder/GroupSource/GroupSource.py
Normal 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
|
||||
|
2
ZopeProducts/exUserFolder/GroupSource/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2002/12/02 23:20:49 bmh Exp $
|
||||
import GroupSource
|
@ -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>
|
31
ZopeProducts/exUserFolder/GroupSources/__init__.py
Normal 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
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:53 akm Exp $
|
||||
import nullGroupSource
|
@ -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>
|
@ -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
|
||||
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:54 akm Exp $
|
||||
import zodbGroupSource
|
@ -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>
|
@ -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>
|
@ -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
|
BIN
ZopeProducts/exUserFolder/I18N/ZBabelDictionary_txt.zexp
Normal file
91
ZopeProducts/exUserFolder/LICENSE
Normal 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.
|
27
ZopeProducts/exUserFolder/LoginRequiredMessages.py
Normal 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',
|
||||
}
|
25
ZopeProducts/exUserFolder/MembershipSources/__init__.py
Normal 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
|
||||
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -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>
|
@ -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>
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
|
||||
import basicMemberSource
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
|
||||
import nullMemberSource
|
@ -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>
|
@ -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
|
||||
|
46
ZopeProducts/exUserFolder/Plugins.py
Normal 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
|
28
ZopeProducts/exUserFolder/PropSources/__init__.py
Normal 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
|
||||
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
.*.swp
|
@ -0,0 +1,2 @@
|
||||
# $Id: __init__.py,v 1.1 2004/11/10 14:15:56 akm Exp $
|
||||
import nullPropSource
|