Progress on implementing alternative usermodel

This commit is contained in:
Marc 2014-09-30 09:49:07 +00:00
parent 5d2606d721
commit 8a0a73a640
60 changed files with 12230 additions and 8459 deletions

16
TODO.md
View file

@ -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
View 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

View file

@ -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,

View file

@ -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):

View file

@ -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()

View file

@ -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)

View file

@ -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 %}

View file

@ -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 = (

View file

@ -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')

View file

@ -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'),
)

View file

@ -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')))

View 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)

View 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)

View 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"]

View 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)

View 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)

View 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')

View file

@ -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)

View file

@ -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)

View file

@ -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"]

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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'
'}'
)

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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')

View file

@ -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)

View file

@ -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 %}

View file

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ app_label|capfirst }}</a>
&rsaquo; <a href="{% url 'admin:users_user_changelist' %}">Users</a>
&rsaquo; <a href="{% url 'admin:users_user_change' object.pk|admin_urlquote %}">{{ user|truncatewords:"18" }}</a>
&rsaquo; <a href="../">{{ role.name|truncatewords:"18" }}</a>
&rsaquo; {% trans 'Delete' %}
</div>
{% endblock %}

View file

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' user.pk %}">{{ user|capfirst }}</a>
&rsaquo; {{ 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 %}

View file

@ -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',

View file

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 48 KiB