Progress on implementing alternative usermodel
16
TODO.md
|
@ -127,3 +127,19 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
|||
|
||||
* prevent deletion of main user by the user itself
|
||||
|
||||
|
||||
|
||||
* AccountAdminMixin auto adds 'account__name' on searchfields and handle account_link on fieldsets
|
||||
|
||||
* account defiition:
|
||||
* identify a customer or a person
|
||||
* has one main system user for running website
|
||||
* pangea staff are different accounts
|
||||
* An account identify a person
|
||||
* Maybe merge users into accounts? again. Account contains main_users, users contains FTP shit
|
||||
* Separate panel from server passwords?
|
||||
* Store passwords on panel?
|
||||
|
||||
|
||||
|
||||
* What fields we really need on contacts? name email phone and what more?
|
||||
|
|
482
docs/services.svg
Normal file
|
@ -0,0 +1,482 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1052.3622"
|
||||
height="744.09448"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
sodipodi:docname="services.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.70710678"
|
||||
inkscape:cx="559.86324"
|
||||
inkscape:cy="278.12745"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-308.2677)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:FreeMono Bold"
|
||||
x="132.85733"
|
||||
y="526.45612"
|
||||
id="text2985"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2987"
|
||||
x="132.85733"
|
||||
y="526.45612">Orders</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="133.94112"
|
||||
y="853.0473"
|
||||
id="text2989"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2991"
|
||||
x="133.94112"
|
||||
y="853.0473"
|
||||
style="font-size:22px;text-align:center;line-height:94.99999880999999391%;text-anchor:middle">Metric</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.6925"
|
||||
y="431.67795"
|
||||
id="text2993"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2995"
|
||||
x="294.6925"
|
||||
y="431.67795">Periodic</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.6925"
|
||||
y="453.67804"
|
||||
id="tspan2997">billing</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.77344"
|
||||
y="597.10419"
|
||||
id="text2999"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3001"
|
||||
x="294.77344"
|
||||
y="597.10419">One-time</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.77344"
|
||||
y="619.10431"
|
||||
id="tspan3003">service</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="472.50183"
|
||||
id="text3005"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007"
|
||||
x="488.67383"
|
||||
y="472.50183">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="494.50192"
|
||||
id="tspan3009">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="390.854"
|
||||
id="text3011"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013"
|
||||
x="488.91711"
|
||||
y="390.854">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="412.8541"
|
||||
id="tspan3015">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.6925"
|
||||
y="758.26892"
|
||||
id="text2993-8"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2995-2"
|
||||
x="294.6925"
|
||||
y="758.26892">Periodic</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.6925"
|
||||
y="780.26904"
|
||||
id="tspan2997-4">billing</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.77344"
|
||||
y="923.69543"
|
||||
id="text2999-4"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3001-2"
|
||||
x="294.77344"
|
||||
y="923.69543">One-time</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.77344"
|
||||
y="945.69556"
|
||||
id="tspan3003-9">service</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="554.14972"
|
||||
id="text3005-7"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-9"
|
||||
x="488.67383"
|
||||
y="554.14972">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="576.14984"
|
||||
id="tspan3009-3">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="635.79749"
|
||||
id="text3011-6"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-2"
|
||||
x="488.91711"
|
||||
y="635.79749">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="657.79761"
|
||||
id="tspan3015-0">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="799.09296"
|
||||
id="text3005-6"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-8"
|
||||
x="488.67383"
|
||||
y="799.09296">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="821.09308"
|
||||
id="tspan3009-2">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="717.44519"
|
||||
id="text3011-1"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-6"
|
||||
x="488.91711"
|
||||
y="717.44519">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="739.44531"
|
||||
id="tspan3015-3">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="962.38898"
|
||||
id="text3005-1"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-3"
|
||||
x="488.67383"
|
||||
y="962.38898">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="984.3891"
|
||||
id="tspan3009-7">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="880.74097"
|
||||
id="text3011-64"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-1"
|
||||
x="488.91711"
|
||||
y="880.74097">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="902.74109"
|
||||
id="tspan3015-08">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="583.38361"
|
||||
y="379.86563"
|
||||
id="text3898"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3900"
|
||||
x="583.38361"
|
||||
y="379.86563"
|
||||
style="font-weight:bold;font-size:22px">Mail accounts</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="583.38361"
|
||||
y="401.86572"
|
||||
id="tspan3912">Concurrent (changes)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="583.38361"
|
||||
y="423.86581"
|
||||
id="tspan3902">Compensate on <tspan
|
||||
style="font-weight:bold;line-height:94.99999880999999391%;-inkscape-font-specification:FreeMono Bold;font-size:22px"
|
||||
id="tspan3904">prepay</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="586.6123"
|
||||
y="461.51324"
|
||||
id="text3906"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3908"
|
||||
x="586.6123"
|
||||
y="461.51324"
|
||||
style="font-weight:bold;font-size:22px">Domains</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="586.6123"
|
||||
y="483.51334"
|
||||
id="tspan3914">Register or renew events</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="586.6123"
|
||||
y="505.51343"
|
||||
id="tspan3910">Compensate on <tspan
|
||||
style="font-weight:bold;line-height:94.99999880999999391%;font-size:22px"
|
||||
id="tspan3993">prepay</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.04663"
|
||||
y="554.09149"
|
||||
id="text3916"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3918"
|
||||
x="590.04663"
|
||||
y="554.09149"
|
||||
style="font-weight:bold;font-size:22px">Plans</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.04663"
|
||||
y="576.09161"
|
||||
id="tspan3920">Always one order</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.58252"
|
||||
y="635.97125"
|
||||
id="text3922"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="635.97125"
|
||||
id="tspan3926"
|
||||
style="font-weight:bold;font-size:22px">CMS installation</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="657.97137"
|
||||
id="tspan3930">Register or renew events</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="679.97144"
|
||||
id="tspan3932" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.32349"
|
||||
y="777.26685"
|
||||
id="text3934"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3936"
|
||||
x="591.32349"
|
||||
y="777.26685"
|
||||
style="font-weight:bold;font-size:22px">Traffic consumption</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="799.26697"
|
||||
id="tspan3938">Metric period lookup</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="821.26703"
|
||||
id="tspan3940">Prepay and != billing_period</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="843.26715"
|
||||
id="tspan3995"
|
||||
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px"> NotImplemented</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.32349"
|
||||
y="717.61884"
|
||||
id="text3942"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3944"
|
||||
x="591.32349"
|
||||
y="717.61884"
|
||||
style="font-weight:bold;font-size:22px">Mailbox size</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="739.61896"
|
||||
id="tspan3946">Concurrent (changes)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.1192"
|
||||
y="882.65179"
|
||||
id="text3942-8"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3944-2"
|
||||
x="590.1192"
|
||||
y="882.65179"
|
||||
style="font-weight:bold;font-size:22px">Jobs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.1192"
|
||||
y="904.65192"
|
||||
id="tspan3946-6">Last known metric</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.06866"
|
||||
y="973.33081"
|
||||
id="text3972"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3974"
|
||||
x="591.06866"
|
||||
y="973.33081"
|
||||
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px">NotImplement</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 228.73934,436.50836 -23.61913,0 0,164.75267 23.53877,0"
|
||||
id="path4013"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 205.55487,521.14184 -23.98356,0"
|
||||
id="path4019"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 228.73934,764.42561 -23.61913,0 0,164.7526 23.53877,0"
|
||||
id="path4013-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 205.55487,849.05914 -23.98356,0"
|
||||
id="path4019-1"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,398.68928 -23.61908,0 0,80.25672 23.5387,0"
|
||||
id="path4013-6"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,438.12728 -23.98362,0"
|
||||
id="path4019-18"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,561.72165 -23.61908,0 0,80.25664 23.5387,0"
|
||||
id="path4013-6-63"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,601.15965 -23.98362,0"
|
||||
id="path4019-18-4"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,726.60658 -23.61908,0 0,80.25672 23.5387,0"
|
||||
id="path4013-6-8"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,766.04458 -23.98362,0"
|
||||
id="path4019-18-3"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,889.63886 -23.61908,0 0,80.25667 23.5387,0"
|
||||
id="path4013-6-0"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,929.07689 -23.98362,0"
|
||||
id="path4019-18-8"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 21 KiB |
|
@ -9,8 +9,7 @@ from django.utils.six.moves.urllib.parse import parse_qsl
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import (wrap_admin_view, admin_link, set_url_query,
|
||||
change_url)
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query, change_url
|
||||
from orchestra.core import services, accounts
|
||||
|
||||
from .filters import HasMainUserListFilter
|
||||
|
@ -19,7 +18,7 @@ from .models import Account
|
|||
|
||||
|
||||
class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
|
||||
list_display = ('name', 'type', 'is_active')
|
||||
list_display = ('username', 'type', 'is_active')
|
||||
list_filter = (
|
||||
'type', 'is_active', HasMainUserListFilter
|
||||
)
|
||||
|
@ -27,16 +26,28 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
|
|||
(_("User"), {
|
||||
'fields': ('username', 'password1', 'password2',),
|
||||
}),
|
||||
(_("Account info"), {
|
||||
'fields': (('type', 'language'), 'comments'),
|
||||
(_("Personal info"), {
|
||||
'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'),
|
||||
}),
|
||||
(_("Permissions"), {
|
||||
'fields': ('is_superuser', 'is_active')
|
||||
}),
|
||||
(_("Important dates"), {
|
||||
'fields': ('last_login', 'date_joined')
|
||||
}),
|
||||
)
|
||||
fieldsets = (
|
||||
(_("User"), {
|
||||
'fields': ('username', 'password',),
|
||||
'fields': ('username', 'password',)
|
||||
}),
|
||||
(_("Account info"), {
|
||||
'fields': (('type', 'language'), 'comments'),
|
||||
(_("Personal info"), {
|
||||
'fields': ('first_name', 'last_name', 'email', ('type', 'language'), 'comments'),
|
||||
}),
|
||||
(_("Permissions"), {
|
||||
'fields': ('is_superuser', 'is_active')
|
||||
}),
|
||||
(_("Important dates"), {
|
||||
'fields': ('last_login', 'date_joined')
|
||||
}),
|
||||
)
|
||||
search_fields = ('username',)
|
||||
|
@ -45,10 +56,6 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
|
|||
filter_horizontal = ()
|
||||
change_form_template = 'admin/accounts/account/change_form.html'
|
||||
|
||||
def name(self, account):
|
||||
return account.name
|
||||
name.admin_order_field = 'username'
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
if db_field.name == 'comments':
|
||||
|
@ -95,7 +102,7 @@ class AccountListAdmin(AccountAdmin):
|
|||
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||
context = {
|
||||
'url': '../?account=' + str(instance.pk),
|
||||
'name': instance.name
|
||||
'name': instance.username
|
||||
}
|
||||
return '<a href="%(url)s">%(name)s</a>' % context
|
||||
select_account.short_description = _("account")
|
||||
|
@ -143,9 +150,6 @@ class AccountAdminMixin(object):
|
|||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Filter by account """
|
||||
# if db_field.name == 'account':
|
||||
# qs = kwargs.get('queryset', db_field.rel.to.objects)
|
||||
# kwargs['queryset'] = qs.select_related('user')
|
||||
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
if db_field.name in self.filter_by_account_fields:
|
||||
if hasattr(self, 'account'):
|
||||
|
@ -197,7 +201,7 @@ class AccountAdminMixin(object):
|
|||
context.update({
|
||||
'all_selected': False,
|
||||
'title': _("Select %s to change for %s") % (
|
||||
opts.verbose_name, account.name),
|
||||
opts.verbose_name, account.username),
|
||||
})
|
||||
else:
|
||||
request_copy = request.GET.copy()
|
||||
|
@ -247,7 +251,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
|||
self.account = Account.objects.get(**kwargs)
|
||||
opts = self.model._meta
|
||||
context = {
|
||||
'title': _("Add %s for %s") % (opts.verbose_name, self.account.name),
|
||||
'title': _("Add %s for %s") % (opts.verbose_name, self.account.username),
|
||||
'from_account': bool(from_account_id),
|
||||
'account': self.account,
|
||||
'account_opts': Account._meta,
|
||||
|
|
|
@ -5,30 +5,27 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from orchestra.core.validators import validate_password
|
||||
from orchestra.forms.widgets import ReadOnlyWidget
|
||||
|
||||
User = auth.get_user_model()
|
||||
from .models import Account
|
||||
|
||||
|
||||
class AccountCreationForm(auth.forms.UserCreationForm):
|
||||
# class Meta:
|
||||
# model = Account
|
||||
# fields = ("username",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AccountCreationForm, self).__init__(*args, **kwargs)
|
||||
self.fields['password1'].validators.append(validate_password)
|
||||
|
||||
def clean_username(self):
|
||||
# Since User.username is unique, this check is redundant,
|
||||
# but it sets a nicer error message than the ORM. See #13147.
|
||||
# Since model.clean() will check this, this is redundant,
|
||||
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
|
||||
username = self.cleaned_data["username"]
|
||||
try:
|
||||
User._default_manager.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
return username
|
||||
raise forms.ValidationError(self.error_messages['duplicate_username'])
|
||||
|
||||
# def save(self, commit=True):
|
||||
# account = super(auth.forms.UserCreationForm, self).save(commit=False)
|
||||
# account.set_password(self.cleaned_data['password1'])
|
||||
# if commit:
|
||||
# account.save()
|
||||
# return account
|
||||
if hasattr(Account, 'systemusers'):
|
||||
systemuser_model = Account.systemusers.related.model
|
||||
if systemuser_model.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError(self.error_messages['duplicate_username'])
|
||||
return username
|
||||
|
||||
|
||||
class AccountChangeForm(forms.ModelForm):
|
||||
|
|
|
@ -27,4 +27,6 @@ class Command(BaseCommand):
|
|||
email = options.get('email')
|
||||
username = options.get('username')
|
||||
password = options.get('password')
|
||||
Account.objects.create_user(username, email=email, password=password)
|
||||
account = Account.objects.create(name=username)
|
||||
account.main_user = account.users.create_superuser(username, email, password, account=account, is_main=True)
|
||||
account.save()
|
||||
|
|
|
@ -16,16 +16,18 @@ class Account(auth.AbstractBaseUser):
|
|||
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
|
||||
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||
_("Enter a valid username."), 'invalid')])
|
||||
first_name = models.CharField(_("first name"), max_length=30, blank=True)
|
||||
last_name = models.CharField(_("last name"), max_length=30, blank=True)
|
||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
|
||||
max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE)
|
||||
language = models.CharField(_("language"), max_length=2,
|
||||
choices=settings.ACCOUNTS_LANGUAGES,
|
||||
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
||||
registered_on = models.DateField(_("registered"), auto_now_add=True)
|
||||
comments = models.TextField(_("comments"), max_length=256, blank=True)
|
||||
first_name = models.CharField(_("first name"), max_length=30, blank=True)
|
||||
last_name = models.CharField(_("last name"), max_length=30, blank=True)
|
||||
email = models.EmailField(_('email address'), blank=True)
|
||||
is_superuser = models.BooleanField(_("superuser status"), default=False,
|
||||
help_text=_("Designates that this user has all permissions without "
|
||||
"explicitly assigning them."))
|
||||
is_active = models.BooleanField(_("active"), default=True,
|
||||
help_text=_("Designates whether this account should be treated as active. "
|
||||
"Unselect this instead of deleting accounts."))
|
||||
|
@ -43,10 +45,6 @@ class Account(auth.AbstractBaseUser):
|
|||
def name(self):
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def is_superuser(self):
|
||||
return self.pk == settings.ACCOUNTS_MAIN_PK
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return self.is_superuser
|
||||
|
@ -55,11 +53,17 @@ class Account(auth.AbstractBaseUser):
|
|||
def get_main(cls):
|
||||
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
|
||||
|
||||
def clean(self):
|
||||
""" unique usernames between accounts and system users """
|
||||
if not self.pk and hasattr(self, 'systemusers'):
|
||||
if self.systemusers.model.objects.filter(username=self.username).exists():
|
||||
raise validators.ValidationError(_("A user with this name already exists"))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = not self.pk
|
||||
super(Account, self).save(*args, **kwargs)
|
||||
if created:
|
||||
self.users.create(username=self.username, password=self.password)
|
||||
if created and hasattr(self, 'groups'):
|
||||
self.groups.create(name=self.username, account=self)
|
||||
|
||||
def send_email(self, template, context, contacts=[], attachments=[], html=None):
|
||||
contacts = self.contacts.filter(email_usages=contacts)
|
||||
|
|
|
@ -21,28 +21,20 @@
|
|||
|
||||
{% block object-tools-items %}
|
||||
{% if services %}
|
||||
<li><select name="forma" onchange="location = this.options[this.selectedIndex].value;" style="margin: -3px 0 0 0;">
|
||||
<option value="">{% trans "Services:" %}</option>
|
||||
{% for service in services %}
|
||||
<li>
|
||||
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
|
||||
</li>
|
||||
<option value="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}">{{ service.verbose_name_plural|capfirst }}</option>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
|
||||
</select></li>
|
||||
{% endif %}
|
||||
{% if accounts %}
|
||||
<ul class="object-tools">
|
||||
<li><select name="forma" onchange="location = this.options[this.selectedIndex].value;" style="margin: -3px 4px 0px 4px;">
|
||||
<option value="">{% trans "Accounts:" %}</option>
|
||||
{% for account in accounts %}
|
||||
<li>
|
||||
<a href="{% url account|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ account.verbose_name_plural|capfirst }}</a>
|
||||
</li>
|
||||
<option value="{% url account|admin_urlname:'changelist' %}?account={{ original.pk }}">{{ account.verbose_name_plural|capfirst }}</option>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
|
||||
<ul class="object-tools">
|
||||
</select></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
||||
</li>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class ContactAdmin(AccountAdminMixin, admin.ModelAdmin):
|
|||
)
|
||||
list_filter = ('email_usage',)
|
||||
search_fields = (
|
||||
'contact__user__username', 'short_name', 'full_name', 'phone', 'phone2',
|
||||
'contact__account__name', 'short_name', 'full_name', 'phone', 'phone2',
|
||||
'email'
|
||||
)
|
||||
fieldsets = (
|
||||
|
|
|
@ -57,7 +57,7 @@ class PermissionInline(AccountAdminMixin, admin.TabularInline):
|
|||
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = ('name', 'type', 'account_link')
|
||||
list_filter = ('type',)
|
||||
search_fields = ['name', 'account__username']
|
||||
search_fields = ['name']
|
||||
inlines = [UserInline]
|
||||
add_inlines = []
|
||||
change_readonly_fields = ('name', 'type')
|
||||
|
@ -102,7 +102,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
class DatabaseUserAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = ('username', 'type', 'account_link')
|
||||
list_filter = ('type',)
|
||||
search_fields = ['username', 'account__username']
|
||||
search_fields = ['username']
|
||||
form = DatabaseUserChangeForm
|
||||
add_form = DatabaseUserCreationForm
|
||||
change_readonly_fields = ('username', 'type')
|
||||
|
|
|
@ -54,7 +54,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
|||
inlines = [RecordInline, DomainInline]
|
||||
list_filter = [TopDomainListFilter]
|
||||
change_readonly_fields = ('name',)
|
||||
search_fields = ['name', 'account__username']
|
||||
search_fields = ['name',]
|
||||
default_changelist_filters = (
|
||||
('top_domain', 'True'),
|
||||
)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from markdown import markdown
|
||||
|
||||
from orchestra.apps.users.models import User
|
||||
from orchestra.forms.widgets import ReadOnlyWidget
|
||||
|
||||
from .models import Queue, Ticket
|
||||
|
@ -60,7 +60,7 @@ class UsersIterator(forms.models.ModelChoiceIterator):
|
|||
|
||||
def __iter__(self):
|
||||
yield ('', '---------')
|
||||
users = User.objects.exclude(is_active=False).order_by('name')
|
||||
users = get_user_model().objects.exclude(is_active=False).order_by('name')
|
||||
superusers = users.filter(is_superuser=True)
|
||||
if superusers:
|
||||
yield ('Operators', list(superusers.values_list('pk', 'name')))
|
||||
|
|
55
orchestra/apps/systemusers/admin.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.util import unquote
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import wrap_admin_view
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
|
||||
from .forms import UserCreationForm, UserChangeForm
|
||||
from .models import SystemUser
|
||||
|
||||
|
||||
class SystemUserAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = ('username', 'account_link', 'shell', 'home', 'is_active',)
|
||||
list_filter = ('is_active',)
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('username', 'password', 'is_active', 'account_link')
|
||||
}),
|
||||
(_("System"), {
|
||||
'fields': ('home', 'shell', 'groups'),
|
||||
}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'fields': ('username', 'password1', 'password2', 'account')
|
||||
}),
|
||||
(_("System"), {
|
||||
'fields': ('home', 'shell', 'groups'),
|
||||
}),
|
||||
)
|
||||
search_fields = ['username']
|
||||
readonly_fields = ('is_main', 'account_link',)
|
||||
change_readonly_fields = ('username',)
|
||||
filter_horizontal = ('groups',)
|
||||
filter_by_account_fields = ('groups',)
|
||||
add_form = UserCreationForm
|
||||
form = UserChangeForm
|
||||
ordering = ('-id',)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
""" exclude self reference on groups """
|
||||
form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
if obj:
|
||||
# Has to be done here and not in the form because of strange phenomenon
|
||||
# derived from monkeypatching formfield.widget.render on AccountAdminMinxin,
|
||||
# don't ask.
|
||||
formfield = form.base_fields['groups']
|
||||
formfield.queryset = formfield.queryset.exclude(name=obj.username)
|
||||
return form
|
||||
|
||||
|
||||
admin.site.register(SystemUser, SystemUserAdmin)
|
15
orchestra/apps/systemusers/api.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import viewsets
|
||||
|
||||
from orchestra.api import router, SetPasswordApiMixin
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
||||
from .serializers import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
||||
model = get_user_model()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
router.register(r'users', UserViewSet)
|
46
orchestra/apps/systemusers/forms.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from django import forms
|
||||
from django.contrib import auth
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.core.validators import validate_password
|
||||
|
||||
from .models import SystemUser
|
||||
|
||||
|
||||
class UserCreationForm(auth.forms.UserCreationForm):
|
||||
# class Meta:
|
||||
# model = SystemUser
|
||||
# fields = ('username',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserCreationForm, self).__init__(*args, **kwargs)
|
||||
self.fields['password1'].validators.append(validate_password)
|
||||
|
||||
def clean_username(self):
|
||||
# Since model.clean() will check this, this is redundant,
|
||||
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
|
||||
username = self.cleaned_data["username"]
|
||||
account_model = SystemUser.account.field.rel.to
|
||||
if account_model.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError(self.error_messages['duplicate_username'])
|
||||
return username
|
||||
|
||||
|
||||
class UserChangeForm(forms.ModelForm):
|
||||
password = auth.forms.ReadOnlyPasswordHashField(label=_("Password"),
|
||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||
"this user's password, but you can change the password "
|
||||
"using <a href=\"password/\">this form</a>."))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserChangeForm, self).__init__(*args, **kwargs)
|
||||
f = self.fields.get('user_permissions', None)
|
||||
if f is not None:
|
||||
f.queryset = f.queryset.select_related('content_type')
|
||||
|
||||
def clean_password(self):
|
||||
# Regardless of what the user provides, return the initial value.
|
||||
# This is done here, rather than on the field, because the
|
||||
# field does not have access to the initial value
|
||||
return self.initial["password"]
|
87
orchestra/apps/systemusers/models.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from django.contrib.auth.hashers import make_password
|
||||
from django.core import validators
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
class SystemUserQuerySet(models.QuerySet):
|
||||
def create_user(self, username, password='', **kwargs):
|
||||
user = super(SystemUserQuerySet, self).create(username=username, **kwargs)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
|
||||
class SystemUser(models.Model):
|
||||
""" System users """
|
||||
username = models.CharField(_("username"), max_length=64, unique=True,
|
||||
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
|
||||
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||
_("Enter a valid username."), 'invalid')])
|
||||
password = models.CharField(_("password"), max_length=128)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='systemusers')
|
||||
home = models.CharField(_("home"), max_length=256, blank=True,
|
||||
help_text=_("Home directory relative to account's ~primary_user"))
|
||||
shell = models.CharField(_("shell"), max_length=32,
|
||||
choices=settings.USERS_SHELLS, default=settings.USERS_DEFAULT_SHELL)
|
||||
groups = models.ManyToManyField('systemusers.Group', blank=True,
|
||||
help_text=_("A new group will be created for the user. "
|
||||
"Which additional groups would you like them to be a member of?"))
|
||||
is_main = models.BooleanField(_("is main"), default=False)
|
||||
is_active = models.BooleanField(_("active"), default=True,
|
||||
help_text=_("Designates whether this account should be treated as active. "
|
||||
"Unselect this instead of deleting accounts."))
|
||||
|
||||
objects = SystemUserQuerySet.as_manager()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.username
|
||||
|
||||
def clean(self):
|
||||
""" unique usernames between accounts and system users """
|
||||
if not self.pk:
|
||||
field = self._meta.get_field_by_name('account')[0]
|
||||
account_model = field.rel.to
|
||||
if account_model.objects.filter(username=self.username).exists():
|
||||
raise validators.ValidationError(self.error_messages['duplicate_username'])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = not self.pk
|
||||
super(SystemUser, self).save(*args, **kwargs)
|
||||
if created:
|
||||
self.groups.get_or_create(name=self.username, account=self.account)
|
||||
|
||||
def set_password(self, raw_password):
|
||||
self.password = make_password(raw_password)
|
||||
|
||||
def check_password(self, raw_password):
|
||||
"""
|
||||
Returns a boolean of whether the raw_password was correct. Handles
|
||||
hashing formats behind the scenes.
|
||||
"""
|
||||
def setter(raw_password):
|
||||
self.set_password(raw_password)
|
||||
self.save(update_fields=["password"])
|
||||
|
||||
def get_is_active(self):
|
||||
return self.account.is_active and self.is_active
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(_("name"), max_length=64, unique=True,
|
||||
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
|
||||
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||
_("Enter a valid group name."), 'invalid')])
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='groups')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
services.register(SystemUser)
|
35
orchestra/apps/systemusers/serializers.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||
from orchestra.core.validators import validate_password
|
||||
|
||||
|
||||
class UserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
password = serializers.CharField(max_length=128, label=_('Password'),
|
||||
validators=[validate_password], write_only=True, required=False,
|
||||
widget=widgets.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = (
|
||||
'url', 'username', 'password', 'first_name', 'last_name', 'email',
|
||||
'is_admin', 'is_active',
|
||||
)
|
||||
|
||||
def validate_password(self, attrs, source):
|
||||
""" POST only password """
|
||||
if self.object.pk:
|
||||
if 'password' in attrs:
|
||||
raise serializers.ValidationError(_("Can not set password"))
|
||||
elif 'password' not in attrs:
|
||||
raise serializers.ValidationError(_("Password required"))
|
||||
return attrs
|
||||
|
||||
def save_object(self, obj, **kwargs):
|
||||
# FIXME this method will be called when saving nested serializers :(
|
||||
if not obj.pk:
|
||||
obj.set_password(obj.password)
|
||||
super(UserSerializer, self).save_object(obj, **kwargs)
|
16
orchestra/apps/systemusers/settings.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.conf import settings
|
||||
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
|
||||
USERS_SYSTEMUSER_HOME = getattr(settings, 'USERES_SYSTEMUSER_HOME', '/home/%(username)s')
|
||||
|
||||
USERS_FTP_LOG_PATH = getattr(settings, 'USERS_FTP_LOG_PATH', '/var/log/vsftpd.log')
|
||||
|
||||
USERS_SHELLS = getattr(settings, 'USERS_SHELLS', (
|
||||
('/bin/false', _("FTP/sFTP only")),
|
||||
('/bin/rsync', _("rsync shell")),
|
||||
('/bin/bash', "Bash"),
|
||||
))
|
||||
|
||||
USERS_DEFAULT_SHELL = getattr(settings, 'USERS_DEFAULT_SHELL', '/bin/false')
|
|
@ -2,6 +2,7 @@ from django.conf.urls import patterns, url
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.util import unquote
|
||||
from django.contrib.auth import admin as auth
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -10,15 +11,26 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
|
|||
|
||||
from .forms import UserCreationForm, UserChangeForm
|
||||
from .models import User
|
||||
from .roles.filters import role_list_filter_factory
|
||||
|
||||
|
||||
class UserAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = ('username', 'display_is_main')
|
||||
list_filter = ('is_active',)
|
||||
class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
|
||||
list_display = ('username', 'account_link', 'is_main', 'is_superuser', 'is_active')
|
||||
list_filter = ('is_main', 'is_superuser', 'is_active')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('account', 'username', 'password', 'home', 'shell', 'groups', 'is_active')
|
||||
'fields': ('username', 'password', 'account_link')
|
||||
}),
|
||||
(_("System"), {
|
||||
'fields': ('home', 'shell', 'groups'),
|
||||
}),
|
||||
(_("Personal info"), {
|
||||
'fields': ('first_name', 'last_name', 'email')
|
||||
}),
|
||||
(_("Permissions"), {
|
||||
'fields': ('is_main', 'is_active', 'is_superuser')
|
||||
}),
|
||||
(_("Important dates"), {
|
||||
'fields': ('last_login', 'date_joined')
|
||||
}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
|
@ -27,76 +39,25 @@ class UserAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
'fields': ('username', 'password1', 'password2', 'account'),
|
||||
}),
|
||||
)
|
||||
search_fields = ['username', 'account__username']
|
||||
readonly_fields = ('display_is_main', 'account_link')
|
||||
search_fields = ['username']
|
||||
readonly_fields = ('is_main', 'account_link',)
|
||||
change_readonly_fields = ('username',)
|
||||
filter_horizontal = ('groups',)
|
||||
filter_horizontal = ()
|
||||
filter_by_account_fields = ('groups',)
|
||||
add_form = UserCreationForm
|
||||
form = UserChangeForm
|
||||
roles = []
|
||||
ordering = ('-id',)
|
||||
change_form_template = 'admin/users/user/change_form.html'
|
||||
|
||||
def display_is_main(self, instance):
|
||||
return instance.is_main
|
||||
display_is_main.short_description = _("is main")
|
||||
display_is_main.boolean = True
|
||||
|
||||
def get_urls(self):
|
||||
""" Returns the additional urls for the change view links """
|
||||
urls = super(UserAdmin, self).get_urls()
|
||||
opts = self.model._meta
|
||||
new_urls = patterns("")
|
||||
for role in self.roles:
|
||||
new_urls += patterns("",
|
||||
url('^(\d+)/%s/$' % role.url_name,
|
||||
wrap_admin_view(self, role().change_view),
|
||||
name='%s_%s_%s_change' % (opts.app_label, opts.model_name, role.name)),
|
||||
url('^(\d+)/%s/delete/$' % role.url_name,
|
||||
wrap_admin_view(self, role().delete_view),
|
||||
name='%s_%s_%s_delete' % (opts.app_label, opts.model_name, role.name))
|
||||
)
|
||||
return new_urls + urls
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
# TODO implement on AccountAdminMixin
|
||||
fieldsets = super(UserAdmin, self).get_fieldsets(request, obj=obj)
|
||||
if obj and obj.account:
|
||||
fieldsets[0][1]['fields'] = ('account_link',) + fieldsets[0][1]['fields'][1:]
|
||||
return fieldsets
|
||||
|
||||
def get_list_display(self, request):
|
||||
roles = []
|
||||
for role in self.roles:
|
||||
def has_role(user, role_class=role):
|
||||
role = role_class(user=user)
|
||||
if role.exists:
|
||||
return '<img src="/static/admin/img/icon-yes.gif" alt="True">'
|
||||
url = reverse('admin:users_user_%s_change' % role.name, args=(user.pk,))
|
||||
false = '<img src="/static/admin/img/icon-no.gif" alt="False">'
|
||||
return '<a href="%s">%s</a>' % (url, false)
|
||||
has_role.short_description = _("Has %s") % role.name
|
||||
has_role.admin_order_field = role.name
|
||||
has_role.allow_tags = True
|
||||
roles.append(has_role)
|
||||
return list(self.list_display) + roles + ['account_link']
|
||||
|
||||
def get_list_filter(self, request):
|
||||
roles = [ role_list_filter_factory(role) for role in self.roles ]
|
||||
return list(self.list_filter) + roles
|
||||
|
||||
def change_view(self, request, object_id, **kwargs):
|
||||
user = self.get_object(User, unquote(object_id))
|
||||
extra_context = kwargs.get('extra_context', {})
|
||||
extra_context['roles'] = [ role(user=user) for role in self.roles ]
|
||||
kwargs['extra_context'] = extra_context
|
||||
return super(UserAdmin, self).change_view(request, object_id, **kwargs)
|
||||
|
||||
# def get_queryset(self, request):
|
||||
# """ Select related for performance """
|
||||
# related = ['account'] + [ role.name for role in self.roles ]
|
||||
# return super(UserAdmin, self).get_queryset(request).select_related(*related)
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
""" exclude self reference on groups """
|
||||
form = super(AccountAdminMixin, self).get_form(request, obj=obj, **kwargs)
|
||||
if obj:
|
||||
# Has to be done here and not in the form because of strange phenomenon
|
||||
# derived from monkeypatching formfield.widget.render on AccountAdminMinxin,
|
||||
# don't ask.
|
||||
formfield = form.base_fields['groups']
|
||||
formfield.queryset = formfield.queryset.exclude(id=obj.id)
|
||||
return form
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
|
|
@ -10,11 +10,6 @@ from .serializers import UserSerializer
|
|||
class UserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
|
||||
model = get_user_model()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
""" select related roles """
|
||||
qs = super(UserViewSet, self).get_queryset()
|
||||
return qs.select_related(*self.inserted)
|
||||
|
||||
|
||||
router.register(r'users', UserViewSet)
|
||||
|
|
|
@ -47,3 +47,4 @@ class UserChangeForm(forms.ModelForm):
|
|||
# This is done here, rather than on the field, because the
|
||||
# field does not have access to the initial value
|
||||
return self.initial["password"]
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth import models as auth
|
||||
from django.core import validators
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
|
@ -9,47 +10,91 @@ from orchestra.core import services
|
|||
from . import settings
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
""" System users """
|
||||
class User(auth.AbstractBaseUser):
|
||||
username = models.CharField(_("username"), max_length=64, unique=True,
|
||||
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
|
||||
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
||||
"./-/_ only."),
|
||||
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||
_("Enter a valid username."), 'invalid')])
|
||||
password = models.CharField(_("password"), max_length=128)
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='users')
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='users')
|
||||
first_name = models.CharField(_("first name"), max_length=30, blank=True)
|
||||
last_name = models.CharField(_("last name"), max_length=30, blank=True)
|
||||
email = models.EmailField(_("email address"), blank=True)
|
||||
is_superuser = models.BooleanField(_("superuser status"), default=False,
|
||||
help_text=_("Designates that this user has all permissions without "
|
||||
"explicitly assigning them."))
|
||||
is_main = models.BooleanField(_("is main"), default=False)
|
||||
# system_password = models.CharField(_("system password"), max_length=128)
|
||||
home = models.CharField(_("home"), max_length=256, blank=True,
|
||||
help_text=_("Home directory relative to account's ~primary_user"))
|
||||
shell = models.CharField(_("shell"), max_length=32,
|
||||
choices=settings.USERS_SHELLS, default=settings.USERS_DEFAULT_SHELL)
|
||||
groups = models.ManyToManyField('users.User', blank=True,
|
||||
groups = models.ManyToManyField('self', blank=True,
|
||||
help_text=_("A new group will be created for the user. "
|
||||
"Which additional groups would you like them to be a member of?"))
|
||||
is_active = models.BooleanField(_("active"), default=True,
|
||||
help_text=_("Designates whether this account should be treated as active. "
|
||||
"Unselect this instead of deleting accounts."))
|
||||
help_text=_("Designates whether this user should be treated as "
|
||||
"active. Unselect this instead of deleting accounts."))
|
||||
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.username
|
||||
objects = auth.UserManager()
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
@property
|
||||
def is_main(self):
|
||||
return self.username == self.account.username
|
||||
def is_staff(self):
|
||||
return self.is_superuser or self.is_main
|
||||
|
||||
def set_password(self, raw_password):
|
||||
self.password = make_password(raw_password)
|
||||
def get_full_name(self):
|
||||
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||
return full_name.strip() or self.username
|
||||
|
||||
def check_password(self, raw_password):
|
||||
def get_short_name(self):
|
||||
""" Returns the short name for the user """
|
||||
return self.first_name
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
""" Sends an email to this User """
|
||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
"""
|
||||
Returns a boolean of whether the raw_password was correct. Handles
|
||||
hashing formats behind the scenes.
|
||||
Returns True if the user has the specified permission. This method
|
||||
queries all available auth backends, but returns immediately if any
|
||||
backend returns True. Thus, a user who has permission from a single
|
||||
auth backend is assumed to have permission in general. If an object is
|
||||
provided, permissions for this specific object are checked.
|
||||
"""
|
||||
def setter(raw_password):
|
||||
self.set_password(raw_password)
|
||||
self.save(update_fields=["password"])
|
||||
# Active superusers have all permissions.
|
||||
if self.is_active and self.is_superuser:
|
||||
return True
|
||||
# Otherwise we need to check the backends.
|
||||
return auth._user_has_perm(self, perm, obj)
|
||||
|
||||
def get_is_active(self):
|
||||
return self.account.is_active and self.is_active
|
||||
def has_perms(self, perm_list, obj=None):
|
||||
"""
|
||||
Returns True if the user has each of the specified permissions. If
|
||||
object is passed, it checks if the user has all required perms for this
|
||||
object.
|
||||
"""
|
||||
for perm in perm_list:
|
||||
if not self.has_perm(perm, obj):
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
"""
|
||||
Returns True if the user has any permissions in the given app label.
|
||||
Uses pretty much the same logic as has_perm, above.
|
||||
"""
|
||||
# Active superusers have all permissions.
|
||||
if self.is_active and self.is_superuser:
|
||||
return True
|
||||
return auth._user_has_module_perms(self, app_label)
|
||||
#
|
||||
# def set_system_password(self, raw_password):
|
||||
# self.system_password = make_password(raw_password)
|
||||
|
||||
|
||||
services.register(User)
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
from ..models import User
|
||||
|
||||
|
||||
class Register(object):
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._registry
|
||||
|
||||
def register(self, name, model):
|
||||
if name in self._registry:
|
||||
raise KeyError("%s already registered" % name)
|
||||
def has_role(user, model=model):
|
||||
try:
|
||||
getattr(user, name)
|
||||
except model.DoesNotExist:
|
||||
return False
|
||||
return True
|
||||
setattr(User, 'has_%s' % name, has_role)
|
||||
self._registry[name] = model
|
||||
|
||||
def get(self):
|
||||
return self._registry
|
||||
|
||||
|
||||
roles = Register()
|
|
@ -1,145 +0,0 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.admin.util import unquote, get_deleted_objects
|
||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||
from django.db import router
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.utils import get_modeladmin, change_url
|
||||
|
||||
from .forms import role_form_factory
|
||||
from ..models import User
|
||||
|
||||
|
||||
class RoleAdmin(object):
|
||||
model = None
|
||||
name = ''
|
||||
url_name = ''
|
||||
form = None
|
||||
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
try:
|
||||
return getattr(self.user, self.name)
|
||||
except self.model.DoesNotExist:
|
||||
return False
|
||||
|
||||
def get_user(self, request, object_id):
|
||||
try:
|
||||
user = User.objects.get(pk=unquote(object_id))
|
||||
except User.DoesNotExist:
|
||||
opts = self.model._meta
|
||||
raise Http404(
|
||||
_('%(name)s object with primary key %(key)r does not exist.') %
|
||||
{'name': force_text(opts.verbose_name), 'key': escape(object_id)}
|
||||
)
|
||||
return user
|
||||
|
||||
def change_view(self, request, object_id):
|
||||
modeladmin = get_modeladmin(User)
|
||||
user = self.get_user(request, object_id)
|
||||
self.user = user
|
||||
obj = None
|
||||
exists = self.exists
|
||||
if exists:
|
||||
obj = getattr(user, self.name)
|
||||
form_class = self.form if self.form else role_form_factory(self)
|
||||
form = form_class(instance=obj)
|
||||
opts = User._meta
|
||||
app_label = opts.app_label
|
||||
title = _("Add %s for user %s" % (self.name, user))
|
||||
action = _("Create")
|
||||
# User has submitted the form
|
||||
if request.method == 'POST':
|
||||
form = form_class(request.POST, instance=obj)
|
||||
form.user = user
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
context = {
|
||||
'name': obj._meta.verbose_name,
|
||||
'obj': obj,
|
||||
'action': _("saved" if exists else "created")
|
||||
}
|
||||
modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize())
|
||||
msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context
|
||||
modeladmin.message_user(request, msg, messages.SUCCESS)
|
||||
if not "_continue" in request.POST:
|
||||
return redirect(change_url(user))
|
||||
exists = True
|
||||
|
||||
if exists:
|
||||
title = _("Change %s %s settings" % (user, self.name))
|
||||
action = _("Save")
|
||||
form = form_class(instance=obj)
|
||||
|
||||
context = {
|
||||
'title': title,
|
||||
'opts': opts,
|
||||
'app_label': app_label,
|
||||
'form': form,
|
||||
'action': action,
|
||||
'role': self,
|
||||
'roles': [ role(user=user) for role in modeladmin.roles ],
|
||||
'media': modeladmin.media
|
||||
}
|
||||
|
||||
template = 'admin/users/user/role.html'
|
||||
app = modeladmin.admin_site.name
|
||||
return TemplateResponse(request, template, context, current_app=app)
|
||||
|
||||
def delete_view(self, request, object_id):
|
||||
"The 'delete' admin view for this model."
|
||||
opts = self.model._meta
|
||||
app_label = opts.app_label
|
||||
modeladmin = get_modeladmin(User)
|
||||
user = self.get_user(request, object_id)
|
||||
obj = getattr(user, self.name)
|
||||
|
||||
using = router.db_for_write(self.model)
|
||||
|
||||
# Populate deleted_objects, a data structure of all related objects that
|
||||
# will also be deleted.
|
||||
(deleted_objects, perms_needed, protected) = get_deleted_objects(
|
||||
[obj], opts, request.user, modeladmin.admin_site, using)
|
||||
|
||||
if request.POST: # The user has already confirmed the deletion.
|
||||
if perms_needed:
|
||||
raise PermissionDenied
|
||||
obj_display = force_text(obj)
|
||||
modeladmin.log_deletion(request, obj, obj_display)
|
||||
modeladmin.delete_model(request, obj)
|
||||
post_url = change_url(user)
|
||||
preserved_filters = modeladmin.get_preserved_filters(request)
|
||||
post_url = add_preserved_filters(
|
||||
{'preserved_filters': preserved_filters, 'opts': opts}, post_url
|
||||
)
|
||||
return HttpResponseRedirect(post_url)
|
||||
|
||||
object_name = force_text(opts.verbose_name)
|
||||
|
||||
if perms_needed or protected:
|
||||
title = _("Cannot delete %(name)s") % {"name": object_name}
|
||||
else:
|
||||
title = _("Are you sure?")
|
||||
|
||||
context = {
|
||||
"title": title,
|
||||
"object_name": object_name,
|
||||
"object": obj,
|
||||
"deleted_objects": deleted_objects,
|
||||
"perms_lacking": perms_needed,
|
||||
"protected": protected,
|
||||
"opts": opts,
|
||||
"app_label": app_label,
|
||||
'preserved_filters': modeladmin.get_preserved_filters(request),
|
||||
'role': self,
|
||||
}
|
||||
return TemplateResponse(request, 'admin/users/user/delete_role.html',
|
||||
context, current_app=modeladmin.admin_site.name)
|
|
@ -1,23 +0,0 @@
|
|||
from django.contrib.admin import SimpleListFilter
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
def role_list_filter_factory(role):
|
||||
class RoleListFilter(SimpleListFilter):
|
||||
""" Filter Nodes by group according to request.user """
|
||||
title = _("has %s" % role.name)
|
||||
parameter_name = role.url_name
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('True', _("Yes")),
|
||||
('False', _("No")),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'True':
|
||||
return queryset.filter(**{ '%s__isnull' % role.name: False })
|
||||
if self.value() == 'False':
|
||||
return queryset.filter(**{ '%s__isnull' % role.name: True })
|
||||
|
||||
return RoleListFilter
|
|
@ -1,17 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class RoleAdminBaseForm(forms.ModelForm):
|
||||
class Meta:
|
||||
exclude = ('user', )
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.instance.user = self.user
|
||||
return super(RoleAdminBaseForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
def role_form_factory(role):
|
||||
class RoleAdminForm(RoleAdminBaseForm):
|
||||
class Meta(RoleAdminBaseForm.Meta):
|
||||
model = role.model
|
||||
return RoleAdminForm
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
|
||||
from orchestra.admin.utils import insertattr
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
||||
from .models import Jabber
|
||||
|
||||
|
||||
class JabberRoleAdmin(RoleAdmin):
|
||||
model = Jabber
|
||||
name = 'jabber'
|
||||
url_name = 'jabber'
|
||||
|
||||
|
||||
insertattr(get_user_model(), 'roles', JabberRoleAdmin)
|
|
@ -1,15 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .. import roles
|
||||
|
||||
|
||||
class Jabber(models.Model):
|
||||
user = models.OneToOneField('users.User', verbose_name=_("user"),
|
||||
related_name='jabber')
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.user)
|
||||
|
||||
|
||||
roles.register('jabber', Jabber)
|
|
@ -1,124 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import insertattr, admin_link
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
||||
from .forms import MailRoleAdminForm
|
||||
from .models import Mailbox, Address, Autoresponse
|
||||
|
||||
|
||||
class AutoresponseInline(admin.StackedInline):
|
||||
model = Autoresponse
|
||||
verbose_name_plural = _("autoresponse")
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if db_field.name == 'subject':
|
||||
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
||||
return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
#class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||
# list_display = ('email', 'domain_link', 'mailboxes', 'forwards', 'account_link')
|
||||
# fields = ('account_link', ('name', 'domain'), 'destination')
|
||||
# inlines = [AutoresponseInline]
|
||||
# search_fields = ('name', 'domain__name',)
|
||||
# readonly_fields = ('account_link', 'domain_link', 'email_link')
|
||||
# filter_by_account_fields = ['domain']
|
||||
#
|
||||
# domain_link = link('domain', order='domain__name')
|
||||
#
|
||||
# def email_link(self, address):
|
||||
# link = self.domain_link(address)
|
||||
# return "%s@%s" % (address.name, link)
|
||||
# email_link.short_description = _("Email")
|
||||
# email_link.allow_tags = True
|
||||
#
|
||||
# def mailboxes(self, address):
|
||||
# boxes = []
|
||||
# for mailbox in address.get_mailboxes():
|
||||
# user = mailbox.user
|
||||
# url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
|
||||
# boxes.append('<a href="%s">%s</a>' % (url, user.username))
|
||||
# return '<br>'.join(boxes)
|
||||
# mailboxes.allow_tags = True
|
||||
#
|
||||
# def forwards(self, address):
|
||||
# values = [ dest for dest in address.destination.split() if '@' in dest ]
|
||||
# return '<br>'.join(values)
|
||||
# forwards.allow_tags = True
|
||||
#
|
||||
# def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
# if db_field.name == 'destination':
|
||||
# kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
||||
# return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
#
|
||||
# def queryset(self, request):
|
||||
# """ Select related for performance """
|
||||
# qs = super(AddressAdmin, self).queryset(request)
|
||||
# return qs.select_related('domain')
|
||||
|
||||
|
||||
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'email', 'domain_link', 'display_mailboxes', 'display_forward', 'account_link'
|
||||
)
|
||||
fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward')
|
||||
inlines = [AutoresponseInline]
|
||||
search_fields = ('name', 'domain__name',)
|
||||
readonly_fields = ('account_link', 'domain_link', 'email_link')
|
||||
filter_by_account_fields = ['domain']
|
||||
filter_horizontal = ['mailboxes']
|
||||
|
||||
domain_link = admin_link('domain', order='domain__name')
|
||||
|
||||
def email_link(self, address):
|
||||
link = self.domain_link(address)
|
||||
return "%s@%s" % (address.name, link)
|
||||
email_link.short_description = _("Email")
|
||||
email_link.allow_tags = True
|
||||
|
||||
def display_mailboxes(self, address):
|
||||
boxes = []
|
||||
for mailbox in address.mailboxes.all():
|
||||
user = mailbox.user
|
||||
url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
|
||||
boxes.append('<a href="%s">%s</a>' % (url, user.username))
|
||||
return '<br>'.join(boxes)
|
||||
display_mailboxes.short_description = _("Mailboxes")
|
||||
display_mailboxes.allow_tags = True
|
||||
|
||||
def display_forward(self, address):
|
||||
values = [ dest for dest in address.forward.split() ]
|
||||
return '<br>'.join(values)
|
||||
display_forward.short_description = _("Forward")
|
||||
display_forward.allow_tags = True
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if db_field.name == 'forward':
|
||||
kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
|
||||
if db_field.name == 'mailboxes':
|
||||
mailboxes = db_field.rel.to.objects.select_related('user')
|
||||
kwargs['queryset'] = mailboxes.filter(user__account=self.account)
|
||||
return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
def get_queryset(self, request):
|
||||
""" Select related for performance """
|
||||
qs = super(AddressAdmin, self).get_queryset(request)
|
||||
return qs.select_related('domain')
|
||||
|
||||
|
||||
class MailRoleAdmin(RoleAdmin):
|
||||
model = Mailbox
|
||||
name = 'mailbox'
|
||||
url_name = 'mailbox'
|
||||
form = MailRoleAdminForm
|
||||
|
||||
|
||||
admin.site.register(Address, AddressAdmin)
|
||||
insertattr(get_user_model(), 'roles', MailRoleAdmin)
|
|
@ -1,27 +0,0 @@
|
|||
from rest_framework import viewsets
|
||||
|
||||
from orchestra.api import router
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
||||
from .models import Address, Mailbox
|
||||
from .serializers import AddressSerializer, MailboxSerializer
|
||||
|
||||
|
||||
class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||
model = Address
|
||||
serializer_class = AddressSerializer
|
||||
|
||||
|
||||
|
||||
class MailboxViewSet(viewsets.ModelViewSet):
|
||||
model = Mailbox
|
||||
serializer_class = MailboxSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(MailboxViewSet, self).get_queryset()
|
||||
qs = qs.select_related('user')
|
||||
return qs.filter(user__account=self.request.user.account_id)
|
||||
|
||||
|
||||
router.register(r'mailboxes', MailboxViewSet)
|
||||
router.register(r'addresses', AddressViewSet)
|
|
@ -1,160 +0,0 @@
|
|||
import os
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.orchestration import ServiceController
|
||||
from orchestra.apps.resources import ServiceMonitor
|
||||
|
||||
from . import settings
|
||||
from .models import Address
|
||||
|
||||
|
||||
class MailSystemUserBackend(ServiceController):
|
||||
verbose_name = _("Mail system user")
|
||||
model = 'mail.Mailbox'
|
||||
# TODO related_models = ('resources__content_type') ??
|
||||
|
||||
DEFAULT_GROUP = 'postfix'
|
||||
|
||||
def create_user(self, context):
|
||||
self.append(
|
||||
"if [[ $( id %(username)s ) ]]; then \n"
|
||||
" usermod -p '%(password)s' %(username)s \n"
|
||||
"else \n"
|
||||
" useradd %(username)s --password '%(password)s' \\\n"
|
||||
" --shell /dev/null \n"
|
||||
"fi" % context
|
||||
)
|
||||
self.append("mkdir -p %(home)s" % context)
|
||||
self.append("chown %(username)s.%(group)s %(home)s" % context)
|
||||
|
||||
def generate_filter(self, mailbox, context):
|
||||
now = timezone.now().strftime("%B %d, %Y, %H:%M")
|
||||
context['filtering'] = (
|
||||
"# Sieve Filter\n"
|
||||
"# Generated by Orchestra %s\n\n" % now
|
||||
)
|
||||
if mailbox.use_custom_filtering:
|
||||
context['filtering'] += mailbox.custom_filtering
|
||||
else:
|
||||
context['filtering'] += settings.EMAILS_DEFAUL_FILTERING
|
||||
context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
|
||||
self.append("echo '%(filtering)s' > %(filter_path)s" % context)
|
||||
|
||||
def save(self, mailbox):
|
||||
context = self.get_context(mailbox)
|
||||
self.create_user(context)
|
||||
self.generate_filter(mailbox, context)
|
||||
|
||||
def delete(self, mailbox):
|
||||
context = self.get_context(mailbox)
|
||||
self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
|
||||
self.append("killall -u %(username)s" % context)
|
||||
self.append("userdel %(username)s" % context)
|
||||
self.append("rm -fr %(home)s" % context)
|
||||
|
||||
def get_context(self, mailbox):
|
||||
user = mailbox.user
|
||||
context = {
|
||||
'username': user.username,
|
||||
'password': user.password if user.is_active else '*%s' % user.password,
|
||||
'group': self.DEFAULT_GROUP
|
||||
}
|
||||
context['home'] = settings.EMAILS_HOME % context
|
||||
return context
|
||||
|
||||
|
||||
class PostfixAddressBackend(ServiceController):
|
||||
verbose_name = _("Postfix address")
|
||||
model = 'mail.Address'
|
||||
|
||||
def include_virtdomain(self, context):
|
||||
self.append(
|
||||
'[[ $(grep "^\s*%(domain)s\s*$" %(virtdomains)s) ]]'
|
||||
' || { echo "%(domain)s" >> %(virtdomains)s; UPDATED=1; }' % context
|
||||
)
|
||||
|
||||
def exclude_virtdomain(self, context):
|
||||
domain = context['domain']
|
||||
if not Address.objects.filter(domain=domain).exists():
|
||||
self.append('sed -i "s/^%(domain)s//" %(virtdomains)s' % context)
|
||||
|
||||
def update_virtusertable(self, context):
|
||||
self.append(
|
||||
'LINE="%(email)s\t%(destination)s"\n'
|
||||
'if [[ ! $(grep "^%(email)s\s" %(virtusertable)s) ]]; then\n'
|
||||
' echo "$LINE" >> %(virtusertable)s\n'
|
||||
' UPDATED=1\n'
|
||||
'else\n'
|
||||
' if [[ ! $(grep "^${LINE}$" %(virtusertable)s) ]]; then\n'
|
||||
' sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtusertable)s\n'
|
||||
' UPDATED=1\n'
|
||||
' fi\n'
|
||||
'fi' % context
|
||||
)
|
||||
|
||||
def exclude_virtusertable(self, context):
|
||||
self.append(
|
||||
'if [[ $(grep "^%(email)s\s") ]]; then\n'
|
||||
' sed -i "s/^%(email)s\s.*$//" %(virtusertable)s\n'
|
||||
' UPDATED=1\n'
|
||||
'fi'
|
||||
)
|
||||
|
||||
def save(self, address):
|
||||
context = self.get_context(address)
|
||||
self.include_virtdomain(context)
|
||||
self.update_virtusertable(context)
|
||||
|
||||
def delete(self, address):
|
||||
context = self.get_context(address)
|
||||
self.exclude_virtdomain(context)
|
||||
self.exclude_virtusertable(context)
|
||||
|
||||
def commit(self):
|
||||
context = self.get_context_files()
|
||||
self.append('[[ $UPDATED == 1 ]] && { '
|
||||
'postmap %(virtdomains)s;'
|
||||
'postmap %(virtusertable)s;'
|
||||
'}' % context)
|
||||
|
||||
def get_context_files(self):
|
||||
return {
|
||||
'virtdomains': settings.EMAILS_VIRTDOMAINS_PATH,
|
||||
'virtusertable': settings.EMAILS_VIRTUSERTABLE_PATH,
|
||||
}
|
||||
|
||||
def get_context(self, address):
|
||||
context = self.get_context_files()
|
||||
context.update({
|
||||
'domain': address.domain,
|
||||
'email': address.email,
|
||||
'destination': address.destination,
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class AutoresponseBackend(ServiceController):
|
||||
verbose_name = _("Mail autoresponse")
|
||||
model = 'mail.Autoresponse'
|
||||
|
||||
|
||||
class MaildirDisk(ServiceMonitor):
|
||||
model = 'email.Mailbox'
|
||||
resource = ServiceMonitor.DISK
|
||||
verbose_name = _("Maildir disk usage")
|
||||
|
||||
def monitor(self, mailbox):
|
||||
context = self.get_context(mailbox)
|
||||
self.append(
|
||||
"SIZE=$(sed -n '2p' %(maildir_path)s | cut -d' ' -f1)\n"
|
||||
"echo %(object_id)s ${SIZE:-0}" % context
|
||||
)
|
||||
|
||||
def get_context(self, mailbox):
|
||||
context = MailSystemUserBackend().get_context(mailbox)
|
||||
context['home'] = settings.EMAILS_HOME % context
|
||||
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||
context['object_id'] = mailbox.pk
|
||||
return context
|
|
@ -1,53 +0,0 @@
|
|||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.forms.widgets import ReadOnlyWidget
|
||||
|
||||
from .models import Mailbox
|
||||
from ..forms import RoleAdminBaseForm
|
||||
|
||||
|
||||
class MailRoleAdminForm(RoleAdminBaseForm):
|
||||
class Meta(RoleAdminBaseForm.Meta):
|
||||
model = Mailbox
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MailRoleAdminForm, self).__init__(*args, **kwargs)
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
widget = ReadOnlyWidget(self.addresses(instance))
|
||||
self.fields['addresses'] = forms.CharField(widget=widget,
|
||||
label=_("Addresses"))
|
||||
|
||||
# def addresses(self, mailbox):
|
||||
# account = mailbox.user.account
|
||||
# addresses = account.addresses.filter(destination__contains=mailbox.user.username)
|
||||
# add_url = reverse('admin:mail_address_add')
|
||||
# add_url += '?account=%d&destination=%s' % (account.pk, mailbox.user.username)
|
||||
# img = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">'
|
||||
# onclick = 'onclick="return showAddAnotherPopup(this);"'
|
||||
# add_link = '<a href="%s" %s>%s Add address</a>' % (add_url, onclick, img)
|
||||
# value = '%s<br><br>' % add_link
|
||||
# for pk, name, domain in addresses.values_list('pk', 'name', 'domain__name'):
|
||||
# url = reverse('admin:mail_address_change', args=(pk,))
|
||||
# name = '%s@%s' % (name, domain)
|
||||
# value += '<li><a href="%s">%s</a></li>' % (url, name)
|
||||
# value = '<ul>%s</ul>' % value
|
||||
# return mark_safe('<div style="padding-left: 100px;">%s</div>' % value)
|
||||
|
||||
def addresses(self, mailbox):
|
||||
account = mailbox.user.account
|
||||
add_url = reverse('admin:mail_address_add')
|
||||
add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
|
||||
img = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">'
|
||||
onclick = 'onclick="return showAddAnotherPopup(this);"'
|
||||
add_link = '<a href="%s" %s>%s Add address</a>' % (add_url, onclick, img)
|
||||
value = '%s<br><br>' % add_link
|
||||
for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
|
||||
url = reverse('admin:mail_address_change', args=(pk,))
|
||||
name = '%s@%s' % (name, domain)
|
||||
value += '<li><a href="%s">%s</a></li>' % (url, name)
|
||||
value = '<ul>%s</ul>' % value
|
||||
return mark_safe('<div style="padding-left: 100px;">%s</div>' % value)
|
|
@ -1,110 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
|
||||
from .. import roles
|
||||
|
||||
from . import validators, settings
|
||||
|
||||
|
||||
class Mailbox(models.Model):
|
||||
user = models.OneToOneField('users.User', verbose_name=_("User"),
|
||||
related_name='mailbox')
|
||||
use_custom_filtering = models.BooleanField(_("Use custom filtering"),
|
||||
default=False)
|
||||
custom_filtering = models.TextField(_("filtering"), blank=True,
|
||||
validators=[validators.validate_sieve],
|
||||
help_text=_("Arbitrary email filtering in sieve language."))
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("mailboxes")
|
||||
|
||||
def __unicode__(self):
|
||||
return self.user.username
|
||||
|
||||
# def get_addresses(self):
|
||||
# regex = r'(^|\s)+%s(\s|$)+' % self.user.username
|
||||
# return Address.objects.filter(destination__regex=regex)
|
||||
#
|
||||
# def delete(self, *args, **kwargs):
|
||||
# """ Update related addresses """
|
||||
# regex = re.compile(r'(^|\s)+(\s*%s)(\s|$)+' % self.user.username)
|
||||
# super(Mailbox, self).delete(*args, **kwargs)
|
||||
# for address in self.get_addresses():
|
||||
# address.destination = regex.sub(r'\3', address.destination).strip()
|
||||
# if not address.destination:
|
||||
# address.delete()
|
||||
# else:
|
||||
# address.save()
|
||||
|
||||
|
||||
#class Address(models.Model):
|
||||
# name = models.CharField(_("name"), max_length=64,
|
||||
# validators=[validators.validate_emailname])
|
||||
# domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
||||
# verbose_name=_("domain"),
|
||||
# related_name='addresses')
|
||||
# destination = models.CharField(_("destination"), max_length=256,
|
||||
# validators=[validators.validate_destination],
|
||||
# help_text=_("Space separated mailbox names or email addresses"))
|
||||
# account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
# related_name='addresses')
|
||||
#
|
||||
# class Meta:
|
||||
# verbose_name_plural = _("addresses")
|
||||
# unique_together = ('name', 'domain')
|
||||
#
|
||||
# def __unicode__(self):
|
||||
# return self.email
|
||||
#
|
||||
# @property
|
||||
# def email(self):
|
||||
# return "%s@%s" % (self.name, self.domain)
|
||||
#
|
||||
# def get_mailboxes(self):
|
||||
# for dest in self.destination.split():
|
||||
# if '@' not in dest:
|
||||
# yield Mailbox.objects.select_related('user').get(user__username=dest)
|
||||
|
||||
|
||||
class Address(models.Model):
|
||||
name = models.CharField(_("name"), max_length=64,
|
||||
validators=[validators.validate_emailname])
|
||||
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
||||
verbose_name=_("domain"),
|
||||
related_name='addresses')
|
||||
mailboxes = models.ManyToManyField('mail.Mailbox',
|
||||
verbose_name=_("mailboxes"),
|
||||
related_name='addresses', blank=True)
|
||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||
validators=[validators.validate_forward])
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
related_name='addresses')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("addresses")
|
||||
unique_together = ('name', 'domain')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.email
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return "%s@%s" % (self.name, self.domain)
|
||||
|
||||
|
||||
class Autoresponse(models.Model):
|
||||
address = models.OneToOneField(Address, verbose_name=_("address"),
|
||||
related_name='autoresponse')
|
||||
# TODO initial_date
|
||||
subject = models.CharField(_("subject"), max_length=256)
|
||||
message = models.TextField(_("message"))
|
||||
enabled = models.BooleanField(_("enabled"), default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.address
|
||||
|
||||
|
||||
services.register(Address)
|
||||
roles.register('mailbox', Mailbox)
|
|
@ -1,43 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from orchestra.api import router
|
||||
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||
|
||||
from .models import Address, Mailbox
|
||||
|
||||
|
||||
#class AddressSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# class Meta:
|
||||
# model = Address
|
||||
# fields = ('url', 'name', 'domain', 'destination')
|
||||
|
||||
|
||||
class NestedMailboxSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Mailbox
|
||||
fields = ('url', 'use_custom_filtering', 'custom_filtering')
|
||||
|
||||
|
||||
class MailboxSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Mailbox
|
||||
fields = ('url', 'user', 'use_custom_filtering', 'custom_filtering')
|
||||
|
||||
|
||||
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Address
|
||||
fields = ('url', 'name', 'domain', 'mailboxes', 'forward')
|
||||
|
||||
def get_fields(self, *args, **kwargs):
|
||||
fields = super(AddressSerializer, self).get_fields(*args, **kwargs)
|
||||
account = self.context['view'].request.user.account_id
|
||||
mailboxes = fields['mailboxes'].queryset.select_related('user')
|
||||
fields['mailboxes'].queryset = mailboxes.filter(user__account=account)
|
||||
# TODO do it on permissions or in self.filter_by_account_field ?
|
||||
domain = fields['domain'].queryset
|
||||
fields['domain'].queryset = domain .filter(account=account)
|
||||
return fields
|
||||
|
||||
|
||||
router.insert('users', 'mailbox', NestedMailboxSerializer, required=False)
|
|
@ -1,29 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
EMAILS_DOMAIN_MODEL = getattr(settings, 'EMAILS_DOMAIN_MODEL', 'domains.Domain')
|
||||
|
||||
EMAILS_HOME = getattr(settings, 'EMAILS_HOME', '/home/%(username)s/')
|
||||
|
||||
EMAILS_SIEVETEST_PATH = getattr(settings, 'EMAILS_SIEVETEST_PATH', '/dev/shm')
|
||||
|
||||
EMAILS_SIEVETEST_BIN_PATH = getattr(settings, 'EMAILS_SIEVETEST_BIN_PATH',
|
||||
'%(orchestra_root)s/bin/sieve-test')
|
||||
|
||||
|
||||
EMAILS_VIRTUSERTABLE_PATH = getattr(settings, 'EMAILS_VIRTUSERTABLE_PATH',
|
||||
'/etc/postfix/virtusertable')
|
||||
|
||||
|
||||
EMAILS_VIRTDOMAINS_PATH = getattr(settings, 'EMAILS_VIRTDOMAINS_PATH',
|
||||
'/etc/postfix/virtdomains')
|
||||
|
||||
|
||||
EMAILS_DEFAUL_FILTERING = getattr(settings, 'EMAILS_DEFAULT_FILTERING',
|
||||
'require ["fileinto","regex","envelope","vacation","reject","relational","comparator-i;ascii-numeric"];\n'
|
||||
'\n'
|
||||
'if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-Score" "5" {\n'
|
||||
' fileinto "Junk";\n'
|
||||
' discard;\n'
|
||||
'}'
|
||||
)
|
|
@ -1,62 +0,0 @@
|
|||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.core.validators import ValidationError, EmailValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.utils import paths
|
||||
from orchestra.utils.system import run
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
def validate_emailname(value):
|
||||
msg = _("'%s' is not a correct email name" % value)
|
||||
if '@' in value:
|
||||
raise ValidationError(msg)
|
||||
value += '@localhost'
|
||||
try:
|
||||
EmailValidator(value)
|
||||
except ValidationError:
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
#def validate_destination(value):
|
||||
# """ space separated mailboxes or emails """
|
||||
# for destination in value.split():
|
||||
# msg = _("'%s' is not an existent mailbox" % destination)
|
||||
# if '@' in destination:
|
||||
# if not destination[-1].isalpha():
|
||||
# raise ValidationError(msg)
|
||||
# EmailValidator(destination)
|
||||
# else:
|
||||
# from .models import Mailbox
|
||||
# if not Mailbox.objects.filter(user__username=destination).exists():
|
||||
# raise ValidationError(msg)
|
||||
# validate_emailname(destination)
|
||||
|
||||
|
||||
def validate_forward(value):
|
||||
""" space separated mailboxes or emails """
|
||||
for destination in value.split():
|
||||
EmailValidator(destination)
|
||||
|
||||
|
||||
def validate_sieve(value):
|
||||
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
||||
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(value)
|
||||
context = {
|
||||
'orchestra_root': paths.get_orchestra_root()
|
||||
}
|
||||
sievetest = settings.EMAILS_SIEVETEST_BIN_PATH % context
|
||||
test = run(' '.join([sievetest, path, '/dev/null']), display=False)
|
||||
if test.return_code:
|
||||
errors = []
|
||||
for line in test.stderr.splitlines():
|
||||
error = re.match(r'^.*(line\s+[0-9]+:.*)', line)
|
||||
if error:
|
||||
errors += error.groups()
|
||||
raise ValidationError(' '.join(errors))
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
|
||||
from orchestra.admin.utils import insertattr
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
||||
from .models import POSIX
|
||||
|
||||
|
||||
class POSIXRoleAdmin(RoleAdmin):
|
||||
model = POSIX
|
||||
name = 'posix'
|
||||
url_name = 'posix'
|
||||
|
||||
|
||||
insertattr(get_user_model(), 'roles', POSIXRoleAdmin)
|
|
@ -1,22 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .. import roles
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
class POSIX(models.Model):
|
||||
user = models.OneToOneField('users.User', verbose_name=_("user"),
|
||||
related_name='posix')
|
||||
home = models.CharField(_("home"), max_length=256, blank=True,
|
||||
help_text=_("Home directory relative to account's ~primary_user"))
|
||||
shell = models.CharField(_("shell"), max_length=32,
|
||||
choices=settings.POSIX_SHELLS, default=settings.POSIX_DEFAULT_SHELL)
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.user)
|
||||
|
||||
# TODO groups
|
||||
|
||||
roles.register('posix', POSIX)
|
|
@ -1,14 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from orchestra.api import router
|
||||
|
||||
from .models import POSIX
|
||||
|
||||
|
||||
class POSIXSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = POSIX
|
||||
fields = ('home', 'shell')
|
||||
|
||||
|
||||
router.insert('users', 'posix', POSIXSerializer, required=False)
|
|
@ -1,11 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
|
||||
POSIX_SHELLS = getattr(settings, 'POSIX_SHELLS', (
|
||||
('/bin/false', _("FTP/sFTP only")),
|
||||
('/bin/rsync', _("rsync shell")),
|
||||
('/bin/bash', "Bash"),
|
||||
))
|
||||
|
||||
POSIX_DEFAULT_SHELL = getattr(settings, 'POSIX_DEFAULT_SHELL', '/bin/false')
|
|
@ -33,3 +33,4 @@ class UserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeriali
|
|||
if not obj.pk:
|
||||
obj.set_password(obj.password)
|
||||
super(UserSerializer, self).save_object(obj, **kwargs)
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "admin/change_form.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
<li><a href="." class="historylink">{% trans "User" %}</a></li>
|
||||
{% for item in roles %}
|
||||
<li><a href="{{ item.url_name }}/" class="{% if item.exists %}historylink{% else %}addlink{% endif %}" title="{{ item.description }}">{% if item.exists %}{{ item.name.capitalize }}{% else %}Add {{ item.name }}{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
|
||||
</li>
|
||||
{% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "admin/delete_confirmation.html" %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ app_label|capfirst }}</a>
|
||||
› <a href="{% url 'admin:users_user_changelist' %}">Users</a>
|
||||
› <a href="{% url 'admin:users_user_change' object.pk|admin_urlquote %}">{{ user|truncatewords:"18" }}</a>
|
||||
› <a href="../">{{ role.name|truncatewords:"18" }}</a>
|
||||
› {% trans 'Delete' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_urls admin_static admin_modify utils %}
|
||||
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
|
||||
{{ media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block coltype %}colM{% endblock %}
|
||||
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">Home</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' user.pk %}">{{ user|capfirst }}</a>
|
||||
› {{ role.name.capitalize }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block content %}<div id="content-main">
|
||||
{% block object-tools %}
|
||||
<ul class="object-tools">
|
||||
{% block object-tools-items %}
|
||||
<li><a href=".." class="historylink">{% trans "User" %}</a></li>
|
||||
{% for item in roles %}
|
||||
<li><a href="../{{ item.url_name }}/" class="{% if item.exists %}historylink{% else %}addlink{% endif %}" title="{{ item.description }}">{% if item.exists %}{{ item.name.capitalize }}{% else %}Add {{ item.name }}{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
{% url opts|admin_urlname:'history' user.pk|admin_urlquote as history_url %}
|
||||
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
|
||||
</li>
|
||||
{% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
<fieldset class="module aligned wide">
|
||||
{% for field in form %}
|
||||
<div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
|
||||
<div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}"{% elif field.is_checkbox %} class="checkbox-row"{% endif %}>
|
||||
{% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||
{% if field|is_checkbox %}
|
||||
{{ field }} <label for="{{ field.id_for_label }}" class="vCheckboxLabel">{{ field.label }}</label>
|
||||
{% else %}
|
||||
{{ field.label_tag }} {{ field }}
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<p class="help">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<div class="submit-row">
|
||||
<input type="submit" value="{{ action }}" class="default" name="_save" />
|
||||
{% if role.exists %}<p class="deletelink-box"><a href="delete/" class="deletelink">Delete</a></p>{% endif %}
|
||||
<input type="submit" value="{{ action }} and continue editing" name="_continue" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -68,7 +68,8 @@ INSTALLED_APPS = (
|
|||
'orchestra',
|
||||
'orchestra.apps.orchestration',
|
||||
'orchestra.apps.domains',
|
||||
'orchestra.apps.users',
|
||||
'orchestra.apps.systemusers',
|
||||
# 'orchestra.apps.users',
|
||||
# 'orchestra.apps.users.roles.mail',
|
||||
# 'orchestra.apps.users.roles.jabber',
|
||||
# 'orchestra.apps.users.roles.posix',
|
||||
|
@ -150,6 +151,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
|
|||
'orchestra.apps.bills.models.Bill',
|
||||
# 'orchestra.apps.payments.models.PaymentSource',
|
||||
'orchestra.apps.payments.models.Transaction',
|
||||
# 'orchestra.apps.payments.models.TransactionProcess',
|
||||
'orchestra.apps.issues.models.Ticket',
|
||||
),
|
||||
'collapsible': True,
|
||||
|
@ -185,18 +187,19 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
|||
'vps/vps': 'TuxBox.png',
|
||||
'miscellaneous/miscellaneous': 'applications-other.png',
|
||||
'saas/saas': 'saas.png',
|
||||
'systemusers/systemuser': 'roleplaying.png',
|
||||
# Accounts
|
||||
'accounts/account': 'Face-monkey.png',
|
||||
'contacts/contact': 'contact_book.png',
|
||||
'orders/order': 'basket.png',
|
||||
'services/contractedplan': 'Pack.png',
|
||||
'services/contractedplan': 'ContractedPack.png',
|
||||
'services/service': 'price.png',
|
||||
'bills/bill': 'invoice.png',
|
||||
'payments/paymentsource': 'card_in_use.png',
|
||||
'payments/transaction': 'transaction.png',
|
||||
'payments/transactionprocess': 'transactionprocess.png',
|
||||
'issues/ticket': 'Ticket_star.png',
|
||||
# Administration
|
||||
'users/user': 'Mr-potato.png',
|
||||
'djcelery/taskstate': 'taskstate.png',
|
||||
'orchestration/server': 'vps.png',
|
||||
'orchestration/route': 'hal.png',
|
||||
|
|
|
@ -31,10 +31,9 @@ def validate_ipv6_address(value):
|
|||
|
||||
def validate_name(value):
|
||||
"""
|
||||
A single non-empty line of free-form text with no whitespace
|
||||
surrounding it.
|
||||
A single non-empty line of free-form text with no whitespace.
|
||||
"""
|
||||
validators.RegexValidator('^\S.*\S$',
|
||||
validators.RegexValidator('^\w+$',
|
||||
_("Enter a valid name (text without whitspaces)."), 'invalid')(value)
|
||||
|
||||
|
||||
|
|
BIN
orchestra/static/orchestra/icons/ContractedPack.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
4337
orchestra/static/orchestra/icons/ContractedPack.svg
Normal file
After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 96 KiB |
BIN
orchestra/static/orchestra/icons/roleplaying.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
444
orchestra/static/orchestra/icons/roleplaying.svg
Normal file
|
@ -0,0 +1,444 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg11300"
|
||||
height="48"
|
||||
width="48"
|
||||
version="1.0"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
sodipodi:docname="roleplaying.svg"
|
||||
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/roleplaying.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<metadata
|
||||
id="metadata78">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview76"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="24"
|
||||
inkscape:cy="24"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg11300" />
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
id="linearGradient3785">
|
||||
<stop
|
||||
id="stop3787"
|
||||
style="stop-color:#bdbdbd"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3789"
|
||||
style="stop-color:#8c8c8c"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient6954">
|
||||
<stop
|
||||
id="stop6960"
|
||||
style="stop-color:#f5f5f5"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop6962"
|
||||
style="stop-color:#d2d2d2"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3341">
|
||||
<stop
|
||||
id="stop3343"
|
||||
style="stop-color:#fff"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3345"
|
||||
style="stop-color:#fff;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient5060">
|
||||
<stop
|
||||
id="stop5062"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop5064"
|
||||
style="stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3783"
|
||||
y2="25"
|
||||
xlink:href="#linearGradient2490"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="31"
|
||||
y1="45"
|
||||
x1="31" />
|
||||
<radialGradient
|
||||
id="radialGradient3805"
|
||||
xlink:href="#linearGradient3993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="12.313"
|
||||
cx="26.376"
|
||||
gradientTransform="matrix(1.6402 -.13258 .12811 1.5849 -18.396 -5.2274)"
|
||||
r="8" />
|
||||
<radialGradient
|
||||
id="radialGradient3813"
|
||||
xlink:href="#linearGradient3993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="28.66"
|
||||
cx="25"
|
||||
gradientTransform="matrix(1.3125 0 0 .37327 -7.8125 20.302)"
|
||||
r="16" />
|
||||
<linearGradient
|
||||
id="linearGradient3821"
|
||||
y2="5"
|
||||
xlink:href="#linearGradient2490"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="33"
|
||||
y1="45"
|
||||
x1="32" />
|
||||
<radialGradient
|
||||
id="radialGradient3831"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="41.5"
|
||||
cx="32"
|
||||
gradientTransform="matrix(1 0 0 .35714 0 26.679)"
|
||||
r="21" />
|
||||
<radialGradient
|
||||
id="radialGradient3844"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="33.188"
|
||||
cx="30"
|
||||
gradientTransform="matrix(1.528 -7.0647e-7 2.1309e-7 .46087 -15.839 14.705)"
|
||||
r="4.6875" />
|
||||
<linearGradient
|
||||
id="linearGradient3858"
|
||||
y2="38.6"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="43"
|
||||
y1="39"
|
||||
x1="38" />
|
||||
<linearGradient
|
||||
id="linearGradient3866"
|
||||
y2="37.438"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="17"
|
||||
y1="39"
|
||||
x1="22" />
|
||||
<linearGradient
|
||||
id="linearGradient3892"
|
||||
y2="83.5"
|
||||
xlink:href="#linearGradient3341"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="24.875"
|
||||
y1="53.869"
|
||||
x1="24.787" />
|
||||
<linearGradient
|
||||
id="linearGradient3910"
|
||||
y2="27.957"
|
||||
xlink:href="#linearGradient3341"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="30"
|
||||
y1="3.0411"
|
||||
x1="30" />
|
||||
<radialGradient
|
||||
id="radialGradient4191"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="33.188"
|
||||
cx="30"
|
||||
gradientTransform="matrix(1.1409 -4.7098e-7 1.591e-7 .30724 -19.727 16.803)"
|
||||
r="4.6875" />
|
||||
<linearGradient
|
||||
id="linearGradient4197"
|
||||
y2="37.438"
|
||||
xlink:href="#linearGradient5060"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="17"
|
||||
gradientTransform="matrix(.77778 0 0 .76190 -8.3222 4.5857)"
|
||||
y1="39"
|
||||
x1="22" />
|
||||
<radialGradient
|
||||
id="radialGradient4200"
|
||||
xlink:href="#linearGradient6954"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="12.313"
|
||||
cx="26.376"
|
||||
gradientTransform="matrix(1.0934 -0.0911 .085410 1.089 -17.764 2.129)"
|
||||
r="8" />
|
||||
<linearGradient
|
||||
id="linearGradient4202"
|
||||
y2="44.679"
|
||||
xlink:href="#linearGradient3785"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="30"
|
||||
gradientTransform="matrix(.66667 0 0 .68712 -5.5001 5.7208)"
|
||||
y1="5"
|
||||
x1="30" />
|
||||
<radialGradient
|
||||
id="radialGradient4205"
|
||||
xlink:href="#linearGradient6954"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cy="28.66"
|
||||
cx="25"
|
||||
gradientTransform="matrix(1.0208 0 0 .28439 -14.854 20.064)"
|
||||
r="16" />
|
||||
<linearGradient
|
||||
id="linearGradient4207"
|
||||
y2="45"
|
||||
xlink:href="#linearGradient3785"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="30"
|
||||
gradientTransform="matrix(.77778 0 0 .76190 -8.7778 4.5952)"
|
||||
y1="26.407"
|
||||
x1="30" />
|
||||
<linearGradient
|
||||
id="linearGradient4236"
|
||||
y2="29.513"
|
||||
xlink:href="#linearGradient3341"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="29.25"
|
||||
gradientTransform="matrix(.66667 0 0 .68712 -5.5001 5.7208)"
|
||||
y1="-1.049"
|
||||
x1="29.25" />
|
||||
<linearGradient
|
||||
id="linearGradient4248"
|
||||
y2="45.156"
|
||||
xlink:href="#linearGradient3341"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="29.286"
|
||||
gradientTransform="matrix(.77778 0 0 .76190 -8.7778 4.5952)"
|
||||
y1="14.969"
|
||||
x1="29.286" />
|
||||
<linearGradient
|
||||
id="linearGradient3993">
|
||||
<stop
|
||||
id="stop3995"
|
||||
style="stop-color:#a3c0d0"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop4001"
|
||||
style="stop-color:#427da1"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2490">
|
||||
<stop
|
||||
id="stop2492"
|
||||
style="stop-color:#2e4a5a"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop2494"
|
||||
style="stop-color:#6e8796"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3713"
|
||||
y2="9.5"
|
||||
xlink:href="#linearGradient2490"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="40"
|
||||
gradientTransform="translate(-1 .5)"
|
||||
y1="9.5"
|
||||
x1="36" />
|
||||
<linearGradient
|
||||
id="linearGradient3721"
|
||||
y2="9.5"
|
||||
xlink:href="#linearGradient3993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="40.5"
|
||||
gradientTransform="translate(-1 .5)"
|
||||
y1="9.5"
|
||||
x1="35.5" />
|
||||
<linearGradient
|
||||
id="linearGradient3725"
|
||||
y2="9.5"
|
||||
xlink:href="#linearGradient3993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="40.5"
|
||||
gradientTransform="matrix(-1 0 0 1 61.044 .5)"
|
||||
y1="9.5"
|
||||
x1="35.5" />
|
||||
<linearGradient
|
||||
id="linearGradient3727"
|
||||
y2="9.5"
|
||||
xlink:href="#linearGradient2490"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="40"
|
||||
gradientTransform="matrix(-1 0 0 1 61.044 .5)"
|
||||
y1="9.5"
|
||||
x1="36" />
|
||||
<linearGradient
|
||||
id="linearGradient3745"
|
||||
y2="10"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="34"
|
||||
y1="10"
|
||||
x1="30">
|
||||
<stop
|
||||
id="stop3741"
|
||||
style="stop-color:#2a2a2a"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3743"
|
||||
style="stop-color:#2a2a2a;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3753"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="26"
|
||||
y1="11"
|
||||
x1="30">
|
||||
<stop
|
||||
id="stop3749"
|
||||
style="stop-color:#2a2a2a"
|
||||
offset="0" />
|
||||
<stop
|
||||
id="stop3751"
|
||||
style="stop-color:#2a2a2a;stop-opacity:0"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
id="g4280">
|
||||
<g
|
||||
id="g4271">
|
||||
<path
|
||||
id="path3823-4"
|
||||
style="opacity:.7;fill:url(#radialGradient3831)"
|
||||
d="m53 41.5a21 7.5 0 1 1 -42 0 21 7.5 0 1 1 42 0z"
|
||||
transform="matrix(.61905 0 0 .53333 -5.8095 13.867)" />
|
||||
<path
|
||||
id="path3766-9"
|
||||
style="stroke:url(#linearGradient4207);fill:url(#radialGradient4205)"
|
||||
d="m11.056 20.214c0.35794 1.6585 0.55774 2.913 0.26736 4.5714-2.2639 1.8399-8.0538 2.6667-8.0451 4.5714l-0.7783 5.714c0 1.894 5.3975 3.429 12.056 3.429 6.6581 0 12.056-1.535 12.056-3.4286l-0.779-5.714c-0.009-1.63-5.833-2.667-8.069-4.571-0.191-1.54-0.039-3.032 0.292-4.572h-7z" />
|
||||
<path
|
||||
id="path4244"
|
||||
style="opacity:.6;stroke:url(#linearGradient4248);fill:none"
|
||||
d="m12.188 21.188c0.20219 1.214 0.33336 2.3815 0.09375 3.75a0.9746 0.9746 0 0 1 -0.34375 0.59375c-1.3919 1.1312-3.4353 1.7478-5.125 2.4062-0.84487 0.32922-1.6037 0.67122-2.0625 0.96875-0.45885 0.29754-0.49992 0.45413-0.5 0.4375a0.9746 0.9746 0 0 1 0 0.125l-0.7817 5.593c0 0.0098 0.056733 0.21188 0.53125 0.53125s1.2835 0.6753 2.3125 0.96875c2.0579 0.58691 4.9963 0.96875 8.25 0.96875s6.1921-0.38184 8.25-0.96875c1.029-0.29345 1.838-0.64938 2.3125-0.96875 0.47452-0.31937 0.53125-0.52142 0.53125-0.53125l-0.781-5.593a0.9746 0.9746 0 0 1 0 -0.09375c0.0076 0.02268 0.0072 0.07494-0.03125 0.03125-0.064-0.073-0.228-0.228-0.469-0.375-0.483-0.293-1.234-0.624-2.094-0.969-1.719-0.689-3.785-1.363-5.156-2.531a0.9746 0.9746 0 0 1 -0.34375 -0.625c-0.15859-1.2781 0.05632-2.5027 0.25-3.7188h-4.8438z" />
|
||||
<path
|
||||
id="path3764-4"
|
||||
style="stroke:url(#linearGradient4202);stroke-width:.99995;fill:url(#radialGradient4200)"
|
||||
d="m14.5 9.5c-2.7614 0-5 2.1534-5 4.8098 0.021642 0.98308 0.10305 2.1509 0.66667 4.8098 0.33334 1.0307 3.3065 3.9788 3.3334 4.3224 0.64645 0.34356 1.6667 0.34356 2.3333 0 0-0.34356 2.6667-3.2917 3-4.3224 0.63425-2.7634 0.63265-3.7792 0.66667-4.8098 0-2.6564-2.2386-4.8098-5-4.8098z" />
|
||||
<path
|
||||
id="path3848-2"
|
||||
style="opacity:.3;fill:url(#linearGradient4197)"
|
||||
d="m8.0111 37.5c-1.8374-1.51-0.9612-4.863-0.7778-7.01-0.7777 1.524-0.7777 6.096-2.3333 6.096 1.0598 0.349 2.1385 0.736 3.1111 0.914z" />
|
||||
<path
|
||||
id="path3833-8"
|
||||
style="opacity:.2;fill:url(#radialGradient4191)"
|
||||
d="m11 25c0.51244 1.1632 1.8808 2 3.5 2s2.9876-0.83683 3.5-2h-7z" />
|
||||
<path
|
||||
id="path4234"
|
||||
style="opacity:.6;stroke:url(#linearGradient4236);stroke-width:.99995;fill:none"
|
||||
d="m14.5 10.469c-2.2533 0-4.0136 1.7068-4.0312 3.8125-0.000089 0.01056 0 0.02067 0 0.03125 0.0211 0.93643 0.09351 2.0351 0.625 4.5625 0.01529 0.02389 0.17954 0.34219 0.46875 0.71875 0.30382 0.39558 0.70309 0.86483 1.0938 1.3125 0.39066 0.44767 0.79154 0.89421 1.0938 1.25 0.14005 0.16488 0.23914 0.29784 0.34375 0.4375 0.29811 0.08926 0.79165 0.09006 1.125 0 0.08512-0.1271 0.1716-0.26022 0.28125-0.40625 0.2693-0.35864 0.618-0.8012 0.96875-1.25s0.69122-0.91447 0.96875-1.3125c0.25674-0.36821 0.4345-0.64456 0.46875-0.71875 0.0041-0.01799-0.0041-0.01342 0-0.03125 0.60553-2.6509 0.59126-3.5202 0.625-4.5625 0-0.01058 0.000089-0.02069 0-0.03125-0.01766-2.1057-1.778-3.8125-4.0312-3.8125z" />
|
||||
</g>
|
||||
<g
|
||||
id="g3912">
|
||||
<path
|
||||
id="path3723"
|
||||
style="stroke-linejoin:round;stroke:url(#linearGradient3727);fill:url(#linearGradient3725)"
|
||||
d="m25.044 12.5c-1.6875-1-4.4243-7.9698-4-5l1 7 3-2z" />
|
||||
<path
|
||||
id="path3705"
|
||||
style="stroke-linejoin:round;stroke:url(#linearGradient3713);fill:url(#linearGradient3721)"
|
||||
d="m35 12.5c1.6875-1 4.4243-7.9698 4-5l-1 7-3-2z" />
|
||||
<path
|
||||
id="path3823"
|
||||
style="opacity:.7;fill:url(#radialGradient3831)"
|
||||
d="m53 41.5a21 7.5 0 1 1 -42 0 21 7.5 0 1 1 42 0z"
|
||||
transform="matrix(.85714 0 0 .8 2.5714 7.8)" />
|
||||
<path
|
||||
id="path3766"
|
||||
style="stroke:url(#linearGradient3783);fill:url(#radialGradient3813)"
|
||||
d="m25.5 20.5c0.46022 2.1767 0.71709 3.8233 0.34375 6-2.911 2.415-10.355 3.5-10.344 6l-1 7.5c0 2.4853 6.9396 4.5 15.5 4.5s15.5-2.0147 15.5-4.5l-1-7.5c-0.012-2.14-7.5-3.5-10.375-6-0.246-2.02-0.05-3.98 0.375-6h-9z" />
|
||||
<path
|
||||
id="path3894"
|
||||
style="opacity:.6;stroke:url(#linearGradient3892);fill:none"
|
||||
d="m22.531 61c0.30464 1.6903 0.51076 3.2715 0.1875 5.1562a1.0027 1.0027 0 0 1 -0.375 0.625c-1.7218 1.4285-4.3454 2.2543-6.5312 3.125-1.0929 0.43533-2.0659 0.86976-2.6875 1.2812-0.62165 0.41149-0.75057 0.6854-0.75 0.8125a1.0027 1.0027 0 0 1 0 0.125l-1 7.375c0 0.003-0.00011 0.02807 0 0.03125 0.0055 0.15834 0.15008 0.47302 0.78125 0.90625 0.64383 0.44193 1.6943 0.89312 3.0312 1.2812 2.675 0.776 6.487 1.281 10.689 1.281 4.2015 0 8.0137-0.50498 10.688-1.2812 1.3369-0.38813 2.3874-0.83932 3.0312-1.2812 0.63117-0.43323 0.77575-0.74791 0.78125-0.90625v-0.03125l-1-7.375a1.0027 1.0027 0 0 1 0 -0.125c0.000137 0.02464 0.01573 0.0028-0.09375-0.125-0.10948-0.12777-0.33441-0.29902-0.65625-0.5-0.64368-0.40196-1.6457-0.85747-2.75-1.3125-2.209-0.91-4.836-1.838-6.531-3.312a1.0027 1.0027 0 0 1 -0.344 -0.625c-0.212-1.743-0.057-3.441 0.219-5.125h-6.6875z"
|
||||
transform="translate(4.125 -39.5)" />
|
||||
<path
|
||||
id="path3764"
|
||||
style="stroke:url(#linearGradient3821);fill:url(#radialGradient3805)"
|
||||
d="m30 5.5c-4.1421 0-7.5 3.134-7.5 7 0.03246 1.4307 0.15457 3.1304 1 7 0.5 1.5 4.9598 5.5 5 6 0.96967 0.5 2.5 0.5 3.5 0 0-0.5 4-4.5 4.5-6 0.95138-4.0217 0.94897-5.5 1-7 0-3.866-3.3579-7-7.5-7z" />
|
||||
<path
|
||||
id="path3848"
|
||||
style="opacity:.3;fill:url(#linearGradient3866)"
|
||||
d="m21 43.2c-2.3623-1.9822-1.2359-6.3829-1-9.2-1 2-1 8-3 8 1.3626 0.45855 2.7496 0.96579 4 1.2z" />
|
||||
<path
|
||||
id="path3850"
|
||||
style="opacity:.3;fill:url(#linearGradient3858)"
|
||||
d="m39 43.2c2.35-1.7437 1.1567-6.4859 1-9.2 1 2 1 8 3 8-1.3626 0.45855-2.7496 0.96579-4 1.2z" />
|
||||
<path
|
||||
id="path3833"
|
||||
style="opacity:.2;fill:url(#radialGradient3844)"
|
||||
d="m25.312 27c0.6863 1.7448 2.5189 3 4.6875 3s4.0012-1.2552 4.6875-3h-9.375z" />
|
||||
<path
|
||||
id="path3906"
|
||||
style="opacity:0.42;stroke:url(#linearGradient3910);fill:none"
|
||||
d="m30 6.5312c-3.6136 0-6.4504 2.679-6.4688 5.9375 0.000142 0.0063-0.000145 0.02499 0 0.03125 0.03183 1.3739 0.15643 2.9814 0.96875 6.7188 0.04059 0.09664 0.33476 0.58291 0.78125 1.125 0.46068 0.55932 1.0398 1.1939 1.625 1.8125 0.58521 0.61863 1.1773 1.2325 1.625 1.7188 0.22386 0.24314 0.40533 0.4577 0.5625 0.65625 0.01424 0.01799 0.01751 0.04473 0.03125 0.0625 0.57991 0.22794 1.5899 0.23044 2.2188 0 0.14029-0.2036 0.3001-0.44272 0.5-0.6875 0.3998-0.48956 0.91074-1.0678 1.4375-1.6875 0.52676-0.61971 1.0788-1.2822 1.5-1.8438 0.40031-0.53374 0.65761-0.97666 0.71875-1.125 0.0033-0.01388-0.0033-0.01744 0-0.03125 0.92253-3.9093 0.91824-5.2123 0.96875-6.7188 0-0.01056 0.000059-0.0207 0-0.03125-0.019-3.2587-2.855-5.9378-6.469-5.9378z"
|
||||
transform="matrix(1.0045 0 0 1.0034 -.13560 -.051359)" />
|
||||
<path
|
||||
id="path3691"
|
||||
style="opacity:0.42;fill:#fff"
|
||||
d="m26 13c3 1 4 2 4 5v-5h-4z" />
|
||||
<path
|
||||
id="path3695"
|
||||
style="opacity:0.42;fill:#fff"
|
||||
d="m34 13c-3 1-4 2-4 5v-5h4z" />
|
||||
<path
|
||||
id="path3697"
|
||||
style="opacity:0.42;fill:url(#linearGradient3753)"
|
||||
d="m26 12c1.6875-1 4-2 4-5v5h-4z" />
|
||||
<path
|
||||
id="path3699"
|
||||
style="opacity:0.42;fill:url(#linearGradient3745)"
|
||||
d="m34 12c-1.688-1-4-2-4-5v5h4z" />
|
||||
<path
|
||||
id="path3701"
|
||||
style="opacity:0.42;fill:#2a2a2a"
|
||||
d="m26 17c1 1 1 4 4 4h-3l-1-4z" />
|
||||
<path
|
||||
id="path3703"
|
||||
style="opacity:0.42;fill:#2a2a2a"
|
||||
d="m34 17c-1 1-1 4-4 4h3l1-4z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 17 KiB |
BIN
orchestra/static/orchestra/icons/transactionprocess.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
1533
orchestra/static/orchestra/icons/transactionprocess.svg
Normal file
After Width: | Height: | Size: 48 KiB |