Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

659

660

661

662

663

664

665

666

667

668

669

670

671

672

673

674

675

676

677

678

679

680

681

682

683

684

685

686

687

688

689

690

691

692

693

694

695

696

697

698

699

700

701

702

703

704

705

706

707

708

709

710

711

712

713

714

715

716

717

718

719

720

721

722

723

724

725

726

727

728

729

730

731

732

733

734

735

736

737

738

739

740

741

742

743

744

745

746

747

748

749

750

751

752

753

754

755

756

757

758

759

760

761

762

763

764

765

766

767

768

769

770

771

772

773

774

775

776

777

778

779

780

781

782

783

784

785

786

787

788

789

790

791

792

793

794

795

796

797

798

799

800

801

802

803

804

805

806

807

808

809

810

811

812

813

814

815

816

817

818

819

820

821

822

823

824

825

826

827

828

829

830

831

832

833

834

835

836

837

838

839

840

841

842

843

844

845

846

847

848

849

850

851

852

853

854

855

856

857

858

859

860

861

862

863

864

865

866

867

868

869

870

871

872

873

874

875

876

877

878

879

880

881

882

883

884

885

886

887

888

889

890

891

892

893

894

895

896

897

898

899

900

901

902

903

904

905

906

907

908

909

910

911

912

913

914

915

916

917

918

919

920

921

922

923

924

925

926

927

928

929

930

931

932

933

934

935

936

937

938

939

940

941

942

943

944

945

946

947

948

949

950

951

952

953

954

955

956

957

958

959

960

961

962

963

964

965

966

967

968

969

970

971

972

973

974

975

976

977

978

979

980

981

982

983

984

985

986

987

988

989

990

991

992

993

994

995

996

997

998

999

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

1015

1016

1017

1018

1019

1020

1021

1022

1023

1024

1025

1026

1027

1028

1029

1030

1031

1032

1033

1034

1035

1036

1037

1038

1039

1040

1041

1042

1043

1044

1045

1046

1047

1048

1049

1050

1051

1052

1053

1054

1055

1056

1057

1058

1059

1060

1061

1062

1063

1064

1065

1066

1067

1068

1069

1070

1071

1072

1073

1074

1075

1076

1077

1078

1079

1080

1081

1082

1083

1084

1085

1086

1087

1088

1089

1090

1091

1092

1093

1094

1095

1096

1097

1098

1099

1100

1101

1102

1103

1104

1105

1106

1107

1108

1109

1110

1111

1112

1113

1114

1115

1116

1117

1118

1119

1120

1121

1122

1123

1124

1125

1126

1127

1128

1129

1130

1131

1132

1133

1134

1135

1136

1137

1138

1139

1140

1141

1142

1143

1144

1145

1146

1147

1148

1149

1150

1151

1152

1153

1154

1155

1156

1157

1158

1159

1160

# -*- coding: UTF-8 -*- 

# Copyright 2009-2016 Luc Saffre 

# License: BSD (see file COPYING for details) 

 

"""This defines the :class:`Action` class and the :func:`action` 

decorator, together with some of the predefined actions. 

 

 

See also: 

 

- :ref:`dev.actions`. 

- :doc:`/tutorials/actions/index` 

 

 

""" 

from builtins import str 

from past.builtins import basestring 

# import six 

 

import logging 

logger = logging.getLogger(__name__) 

 

from django.utils.translation import ugettext_lazy as _ 

from django.utils.translation import string_concat 

from django.utils.encoding import force_text 

from django.conf import settings 

from django.db import models 

 

from lino import AFTER17, AFTER18 

 

if AFTER17: 

    from django.apps import apps 

    get_models = apps.get_models 

else: 

    from django.db.models.loading import get_models 

 

 

from lino.core import constants 

from lino.core.utils import obj2unicode 

from lino.core.utils import resolve_model 

from lino.core import layouts 

from lino.core import fields 

from lino.core import keyboard 

from lino.core.signals import on_ui_created, pre_ui_delete, pre_ui_save 

from lino.core.utils import ChangeWatcher 

from lino.core.permissions import Permittable 

from lino.core.utils import Parametrizable, InstanceAction 

# from lino.modlib.users.choicelists import SiteUser 

from lino.utils.choosers import Chooser 

from lino.utils.xmlgen.html import E 

 

 

def check_for_chooser(holder, field): 

    # holder is either a Model, an Actor or an Action. 

    if isinstance(field, fields.DummyField): 

        return 

    methname = field.name + "_choices" 

    m = getattr(holder, methname, None) 

    if m is not None: 

        ch = Chooser(holder, field, m) 

        d = holder.__dict__.get('_choosers_dict', None) 

        if d is None: 

            d = dict() 

            setattr(holder, '_choosers_dict', d) 

        if ch in d: 

            raise Exception("Redefinition of chooser %s" % field) 

        d[field.name] = ch 

    # if field.name == 'city': 

    #     logger.info("20140822 chooser for %s.%s", holder, field.name) 

 

 

def discover_choosers(): 

    logger.debug("Discovering choosers for model fields...") 

    #~ logger.debug("Instantiate model reports...") 

    for model in get_models(): 

        #~ n = 0 

        if AFTER17: 

            allfields = model._meta.fields 

        else: 

            allfields = model._meta.fields + model._meta.virtual_fields 

        for field in allfields: 

            check_for_chooser(model, field) 

        #~ logger.debug("Discovered %d choosers in model %s.",n,model) 

 

 

def install_layout(cls, k, layout_class, **options): 

    """ 

    - `cls` is the actor (a class object) 

    - `k` is one of 'detail_layout', 'insert_layout', 'params_layout' 

    - `layout_class` 

 

    """ 

    dl = cls.__dict__.get(k, None) 

    if dl is None:  # and not cls._class_init_done: 

        dl = getattr(cls, k) 

    if dl is None: 

        return 

    if isinstance(dl, basestring): 

        setattr(cls, k, layout_class(dl, cls, **options)) 

    elif isinstance(dl, layouts.Panel): 

        options.update(dl.options) 

        setattr(cls, k, layout_class(dl.desc, cls, **options)) 

    elif dl._datasource is None: 

        dl.set_datasource(cls) 

        setattr(cls, k, dl) 

    elif not issubclass(cls, dl._datasource): 

        raise Exception( 

            "Cannot reuse %s instance (%s of %r) for %r" % 

            (dl.__class__, k, dl._datasource, cls)) 

 

 

def register_params(cls): 

    """Note that `cls` is either an actor or an action. And remember that 

    actors are class objects while actions are instances. 

 

    """ 

    if cls.parameters: 

        for k, v in list(cls.parameters.items()): 

            v.set_attributes_from_name(k) 

            v.table = cls 

 

        if cls.params_layout is None: 

            cls.params_layout = cls._layout_class.join_str.join( 

                list(cls.parameters.keys())) 

        install_layout(cls, 'params_layout', cls._layout_class) 

 

    elif cls.params_layout is not None: 

        raise Exception("params_layout but no parameters ?!") 

 

 

def setup_params_choosers(self): 

    if self.parameters: 

        for k, fld in list(self.parameters.items()): 

            if isinstance(fld, models.ForeignKey): 

                # Before Django 1.8: 

                if AFTER18: 

                    fld.rel.model = resolve_model(fld.rel.model) 

                else: 

                    fld.rel.to = resolve_model(fld.rel.to) 

                from lino.core.kernel import set_default_verbose_name 

                set_default_verbose_name(fld) 

                #~ if fld.verbose_name is None: 

                    #~ fld.verbose_name = fld.rel.model._meta.verbose_name 

 

            check_for_chooser(self, fld) 

 

 

def make_params_layout_handle(self, ui): 

    return self.params_layout.get_layout_handle( 

        settings.SITE.kernel.default_ui) 

 

 

from django.utils.encoding import python_2_unicode_compatible 

 

 

@python_2_unicode_compatible 

class Action(Parametrizable, Permittable): 

    """ 

    Abstract base class for all actions. 

    """ 

 

    #~ __metaclass__ = ActionMetaClass 

    _layout_class = layouts.ActionParamsLayout 

 

    label = None 

    """ 

    The text to appear on the button. 

    """ 

    debug_permissions = False 

    save_action_name = None 

    disable_primary_key = True 

    """Whether primary key fields should be disabled when using this 

    action. This is `True` for all actions except :class:`InsertRow`. 

 

    """ 

 

    icon_name = None 

    """The class name of an icon to be used for this action when rendered 

    as toolbar button. 

 

    Allowed icon names are defined in 

    :data:`lino.core.constants.ICON_NAMES`. 

 

    """ 

 

    hidden_elements = frozenset() 

    combo_group = None 

    """ 

    The name of another action to which to "attach" this action. 

    Both actions will then be rendered as a single combobutton. 

 

    """ 

 

    parameters = None 

    "See :attr:`Parametrizable.parameters`." 

 

    use_param_panel = False 

    """Used internally. This is True for window actions whose window use 

    the parameter panel: grid and emptytable (but not showdetail) 

 

    """ 

 

    no_params_window = False 

    """Set this to `True` if your action has :attr:`parameters` but you 

    do *not* want it to open a window where the user can edit these 

    parameters before calling the action. 

 

    Setting this attribute to `True` means that the calling code must 

    explicitly set all parameter values.  Usage example is the 

    :attr:`lino.modlib.polls.models.AnswersByResponse.answer_buttons` 

    virtual field. 

 

    """ 

 

    sort_index = 90 

    """ 

    Determins the sort order in which the actions will be presented to 

    the user. 

 

    List actions are negative and come first. 

 

    Predefined `sort_index` values are: 

 

    ===== ================================= 

    value action 

    ===== ================================= 

    -1    :class:`as_pdf <lino.utils.appy_pod.PrintTableAction>` 

    10    :class:`InsertRow`, :class:`SubmitDetail` 

    11    :attr:`duplicate <lino.mixins.duplicable.Duplicable.duplicate>` 

    20    :class:`detail <ShowDetailAction>` 

    30    :class:`delete <DeleteSelected>` 

    31    :class:`merge <lino.core.merge.MergeAction>` 

    50    :class:`Print <lino.mixins.printable.BasePrintAction>` 

    51    :class:`Clear Cache <lino.mixins.printable.ClearCacheAction>` 

    60    :class:`ShowSlaveTable` 

    90    default for all custom row actions 

    ===== ================================= 

 

    """ 

 

    help_text = None 

    """A help text that shortly explains what this action does. 

    :mod:`lino.modlib.extjs` shows this as tooltip text. 

 

    """ 

 

    auto_save = True 

    """ 

    What to do when this action is being called while the user is on a 

    dirty record. 

     

    - `False` means: forget any changes in current record and run the 

      action. 

 

    - `True` means: save any changes in current record before running 

      the action.  `None` means: ask the user. 

 

    """ 

 

    extjs_main_panel = None 

    """Used by :mod:`lino_xl.lib.extensible` and 

    :mod:`lino.modlib.awesome_uploader`. 

 

    Example:: 

 

        class CalendarAction(dd.Action): 

            extjs_main_panel = "Lino.CalendarApp().get_main_panel()" 

            ... 

 

    """ 

 

    js_handler = None 

    """ 

    This is usually `None`. Otherwise it is the name of a Javascript 

    callable to be called without arguments. That callable must have 

    been defined in a :attr:`lino.core.plugin.Plugin.site_js_snippets` of the plugin. 

 

    """ 

 

    action_name = None 

    """Internally used to store the name of this action within the 

    defining Actor's namespace. 

 

    """ 

    defining_actor = None 

    """Internally used to store the :class:`lino.core.actors.Actor` who 

    defined this action. 

 

    """ 

 

    key = None 

    """ 

    The hotkey to associate to this action in a user interface. 

    """ 

 

    default_format = 'html' 

    """ 

    Used internally. 

    """ 

 

    readonly = True 

    """Whether this action is readonly, i.e. does not change any data. 

 

    Setting this to `False` will make the action unavailable for 

    `readonly` user profiles and will cause it to be logged when 

    :attr:`log_each_action_request 

    <lino.core.site.Site.log_each_action_request>` is set to `True`. 

     

    Note that Lino actually does not check whether it is true. When a 

    readonly action actually does modify the database, Lino won't 

    "notice" it. 

 

    Discussion 

     

    Maybe we should change the name `readonly` to `modifying` or 

    `writing` (and set the default value `False`).  Because for the 

    application developer that looks more natural.  Or --maybe better 

    but probably even more consequences-- the default value should be 

    `False`.  Because being readonly, for actions, is a kind of 

    "privilege": they don't get logged, they also exists for readonly 

    users.  It would be more "secure" when the developer must be 

    explicit when granting that privilege. 

 

    """ 

 

    opens_a_window = False 

    """ 

    Used internally to say whether this action opens a window. 

    """ 

 

    hide_top_toolbar = False 

    """Used internally if :attr:`opens_a_window` to say whether the 

    window has a top toolbar. 

 

    """ 

 

    hide_navigator = False 

    """Used internally if :attr:`opens_a_window` to say whether the 

    window has a navigator. 

 

    """ 

 

    show_in_bbar = True 

    """Whether this action should be displayed as a button in the toolbar 

    and the context menu. 

 

    For example the :class:`CheckinVisitor 

    <lino_xl.lib.reception.models.CheckinVisitor>`, 

    :class:`ReceiveVisitor 

    <lino_xl.lib.reception.models.ReceiveVisitor>` and 

    :class:`CheckoutVisitor 

    <lino_xl.lib.reception.models.CheckoutVisitor>` actions have this 

    attribute explicitly set to `False` because otherwise they would be 

    visible in the toolbar. 

 

    """ 

 

    show_in_workflow = False 

    """Used internally.  Whether this action should be displayed as the 

    :attr:`workflow_buttons <lino.core.model.Model.workflow_buttons>` 

    column. If this is True, then Lino will automatically set 

    :attr:`custom_handler` to True. 

 

    """ 

 

    custom_handler = False 

    """ 

    Whether this action is implemented as Javascript function call. 

    This is necessary if you want your action to be callable using an 

    "action link" (html button). 

 

    """ 

 

    select_rows = True 

    """True if this action needs an object to act on. 

 

    Set this to `False` if this action is a list action, not a row 

    action. 

 

    """ 

 

    http_method = 'GET' 

    """ 

    HTTP method to use when this action is called using an AJAX call. 

    """ 

 

    preprocessor = 'null'  # None 

    """ 

    Name of a Javascript function to be invoked on the web client when 

    this action is called. 

    """ 

 

    hide_virtual_fields = False 

 

    required_states = None 

 

    def __init__(self, label=None, **kw): 

        """The first argument is the optional `label`, other arguments should 

        be specified as keywords and can be any of the existing class 

        attributes. 

 

        """ 

        if label is not None: 

            self.label = label 

 

        # if self.parameters is not None and self.select_rows: 

        #     self.show_in_bbar = False 

        #     # see ticket #105 

 

        for k, v in list(kw.items()): 

            if not hasattr(self, k): 

                raise Exception("Invalid action keyword %s" % k) 

            setattr(self, k, v) 

 

        if self.show_in_workflow: 

            self.custom_handler = True 

 

        if self.icon_name: 

            if not self.icon_name in constants.ICON_NAMES: 

                raise Exception( 

                    "Unkonwn icon_name '{0}'".format(self.icon_name)) 

 

        register_params(self) 

 

    def __get__(self, instance, owner): 

        """ 

        When a model has an action "foo", then getting an attribute 

        "foo" of a model instance will return an :class:`InstanceAction`. 

        """ 

        if instance is None: 

            return self 

        return InstanceAction( 

            self, instance.get_default_table(), instance, owner) 

 

    def is_callable_from(self, caller): 

        return isinstance(caller, (GridEdit, ShowDetailAction)) 

        #~ if self.select_rows: 

            #~ return isinstance(caller,(GridEdit,ShowDetailAction)) 

        #~ return isinstance(caller,GridEdit) 

 

    def is_window_action(self): 

        """Return `True` if this is a "window action" (i.e. which opens a GUI 

        window on the client before executin). 

 

        """ 

        return self.opens_a_window or ( 

            self.parameters and not self.no_params_window) 

 

    def get_status(self, ar, **kw): 

        if self.parameters is not None: 

            defaults = kw.get('field_values', {}) 

            pv = self.params_layout.params_store.pv2dict( 

                ar.action_param_values, **defaults) 

            kw.update(field_values=pv) 

        return kw 

 

    def get_chooser_for_field(self, fieldname): 

        d = getattr(self, '_choosers_dict', {}) 

        return d.get(fieldname, None) 

 

    def get_choices_text(self, obj, request, field): 

        return obj.get_choices_text(request, self, field) 

 

    def make_params_layout_handle(self, ui): 

        return make_params_layout_handle(self, ui) 

 

    def get_data_elem(self, name): 

        # same as in Actor but here it is an instance method 

        return None 

 

    def get_param_elem(self, name): 

        # same as in Actor but here it is an instance method 

        if self.parameters: 

            return self.parameters.get(name, None) 

        return None 

 

    def get_widget_options(self, name, **options): 

        # same as in Actor but here it is an instance method 

        return options 

 

    def get_button_label(self, actor): 

        if actor is None or actor.default_action is None: 

            return self.label 

        if self is actor.default_action.action: 

            return actor.label 

        else: 

            return self.label 

            # since 20140923 return u"%s %s" % (self.label, actor.label) 

 

    def full_name(self, actor): 

        if self.action_name is None: 

            raise Exception("Tried to full_name() on %r" % self) 

            #~ return repr(self) 

        if self.parameters and not self.no_params_window: 

            return self.defining_actor.actor_id + '.' + self.action_name 

        return str(actor) + '.' + self.action_name 

 

    def get_action_title(self, ar): 

        return ar.get_title() 

 

    def __repr__(self): 

        if self.label is None: 

            return "<%s %s>" % (self.__class__.__name__, self.action_name) 

        return "<%s %s (%r)>" % ( 

            self.__class__.__name__, self.action_name, str(self.label)) 

 

    def unused__str__(self): 

        raise Exception("20121003 Must use full_name(actor)") 

        if self.defining_actor is None: 

            return repr(self) 

        if self.action_name is None: 

            return repr(self) 

        return str(self.defining_actor) + ':' + self.action_name 

 

    #~ def set_permissions(self,*args,**kw) 

        #~ self.permission = perms.factory(*args,**kw) 

 

    def attach_to_workflow(self, wf, name): 

        assert self.action_name is None 

        self.action_name = name 

        self.defining_actor = wf 

        setup_params_choosers(self) 

 

    def attach_to_actor(self, actor, name): 

        """Called once per Actor per Action on startup before a BoundAction 

        instance is being created.  If this returns False, then the 

        action won't be attached to the given actor. 

 

        """ 

        # if not actor.editable and not self.readonly: 

        #     return False 

 

        if self.defining_actor is not None: 

            # already defined in another actor 

            return True 

        if self.action_name is not None: 

            raise Exception("tried to attach named action %s.%s" % 

                            (actor, self.action_name)) 

        self.action_name = name 

        self.defining_actor = actor 

        if self.label is None: 

            self.label = name 

        setup_params_choosers(self) 

        # setup_params_choosers(self.__class__) 

        return True 

 

    def __str__(self): 

        # return force_text(self.label) 

        return str(self.label) 

 

    def get_action_permission(self, ar, obj, state): 

        """Return (True or False) whether the given :class:`ActionRequest 

        <lino.core.requests.BaseRequest>` `ar` should get permission 

        to execute on the given Model instance `obj` (which is in the 

        given `state`). 

 

        Derived Action classes may override this to add vetos. 

        E.g. the MoveUp action of a Sequenced is not available on the 

        first row of given `ar`. 

 

        """ 

        return True 

 

    def get_view_permission(self, profile): 

        """ 

        Return True if this action is visible for users of given profile. 

 

        """ 

        return True 

 

    def run_from_ui(self, ar, **kw): 

        """Execute the action.  `ar` is an :class:`ActionRequest 

        <lino.core.requests.BaseRequest>` object representing the 

        context in which the action is running. 

        """ 

        raise NotImplementedError( 

            "%s has no run_from_ui() method" % self.__class__) 

 

    def run_from_code(self, ar, *args, **kw): 

        self.run_from_ui(ar, *args, **kw) 

 

    def run_from_session(self, ses, *args, **kw):  # 20130820 

        if len(args): 

            obj = args[0] 

        else: 

            obj = None 

        ia = InstanceAction(self, self.defining_actor, obj, None) 

        return ia.run_from_session(ses, **kw) 

 

    def action_param_defaults(self, ar, obj, **kw): 

        """Same as :meth:`lino.core.actors.Actor.param_defaults`, except that 

        on an action it is a instance method. 

 

        Note that this method is not called for actions which are rendered 

        in a toolbar (:srcref:`docs/tickets/105`) 

 

        """ 

 

        for k, pf in list(self.parameters.items()): 

            # print 20151203, pf.name, repr(pf.rel.to) 

            kw[k] = pf.get_default() 

        return kw 

 

    def setup_action_request(self, actor, ar): 

        pass 

 

 

class TableAction(Action): 

 

    def get_action_title(self, ar): 

        return ar.get_title() 

 

 

class RedirectAction(Action): 

 

    def get_target_url(self, elem): 

        raise NotImplementedError 

 

 

class GridEdit(TableAction): 

    """Open a window with a grid editor on this table as main item. 

 

    """ 

    use_param_panel = True 

    show_in_workflow = False 

    opens_a_window = True 

    action_name = 'grid' 

 

    def is_callable_from(self, caller): 

        return False 

 

    def attach_to_actor(self, actor, name): 

        #~ self.label = actor.button_label or actor.label 

        self.label = actor.label 

        return super(GridEdit, self).attach_to_actor(actor, name) 

 

    def get_window_layout(self, actor): 

        #~ return self.actor.list_layout 

        return None 

 

    def get_window_size(self, actor): 

        return actor.window_size 

 

 

class ShowDetailAction(Action): 

    """Open the detail window on a row of this table. 

 

    """ 

    icon_name = 'application_form' 

    opens_a_window = True 

    show_in_workflow = False 

    save_action_name = 'submit_detail' 

 

    sort_index = 20 

 

    def is_callable_from(self, caller): 

        return isinstance(caller, GridEdit) 

 

    action_name = 'detail' 

    label = _("Detail") 

    help_text = _("Open a detail window on this record") 

 

    def get_window_layout(self, actor): 

        return actor.detail_layout 

 

    def get_window_size(self, actor): 

        wl = self.get_window_layout(actor) 

        return wl.window_size 

 

 

class ShowEmptyTable(ShowDetailAction): 

    use_param_panel = True 

    action_name = 'show' 

    default_format = 'html' 

    #~ hide_top_toolbar = True 

    hide_navigator = True 

    icon_name = None 

 

    def is_callable_from(self, caller): 

        return isinstance(caller, GridEdit) 

 

    def attach_to_actor(self, actor, name): 

        self.label = actor.label 

        return super(ShowEmptyTable, self).attach_to_actor(actor, name) 

 

    def as_bootstrap_html(self, ar): 

        return super(ShowEmptyTable, self).as_bootstrap_html(ar, '-99998') 

 

 

class InsertRow(TableAction): 

    """Open the Insert window filled with a row of blank or default 

    values.  The new row will be actually created only when this 

    window gets submitted. 

 

    """ 

    save_action_name = 'submit_insert' 

 

    disable_primary_key = False 

 

    label = _("New") 

    icon_name = 'add'  # if action rendered as toolbar button 

    show_in_workflow = False 

    opens_a_window = True 

    hide_navigator = True 

    sort_index = 10 

    hide_top_toolbar = True 

    help_text = _("Insert a new record") 

    # required_roles = set([SiteUser]) 

    action_name = 'insert' 

    key = keyboard.INSERT  # (ctrl=True) 

    hide_virtual_fields = True 

    readonly = False 

    select_rows = False 

 

    def get_action_title(self, ar): 

        return _("Insert into %s") % force_text(ar.get_title()) 

 

    def get_window_layout(self, actor): 

        return actor.insert_layout or actor.detail_layout 

 

    def get_window_size(self, actor): 

        wl = self.get_window_layout(actor) 

        return wl.window_size 

 

    def unused_get_action_permission(self, ar, obj, state): 

        # see blog/2012/0726 

        # if settings.SITE.user_model and ar.get_user().profile.readonly: 

        if ar.get_user().profile.readonly: 

            return False 

        return super(InsertRow, self).get_action_permission(ar, obj, state) 

 

    def get_status(self, ar, **kw): 

        kw = super(InsertRow, self).get_status(ar, **kw) 

        if 'record_id' in kw: 

            return kw 

        if 'data_record' in kw: 

            return kw 

        # raise Exception("20150218 %s" % self) 

        elem = ar.create_instance() 

        # existing = getattr(ar, '_elem', None) 

        # if existing is not None: 

        #     raise Exception("20150218 %s %s", elem, existing) 

        #     if existing == elem: 

        #         return kw 

        # ar._elem = elem 

        rec = ar.elem2rec_insert(ar.ah, elem) 

        kw.update(data_record=rec) 

        return kw 

 

 

class UpdateRowAction(Action): 

    show_in_workflow = False 

    readonly = False 

    # required_roles = set([SiteUser]) 

 

 

class SaveRow(Action): 

    """ 

    Called when user edited a cell of a non-phantom record in a grid. 

    Installed as `update_action` on every :class:`Actor`. 

 

    """ 

    sort_index = 10 

    show_in_workflow = False 

    action_name = 'grid_put' 

    readonly = False 

    auto_save = False 

 

    def is_callable_from(self, caller): 

        return False 

 

    def run_from_ui(self, ar, **kw): 

        # logger.info("20140423 SubmitDetail") 

        elem = ar.selected_rows[0] 

        # ar.form2obj_and_save(ar.rqdata, elem, False) 

        self.save_existing_instance(elem, ar) 

 

    def save_existing_instance(self, elem, ar): 

        watcher = ChangeWatcher(elem) 

        ar.ah.store.form2obj(ar, ar.rqdata, elem, False) 

        elem.full_clean() 

 

        if watcher.is_dirty(): 

            pre_ui_save.send(sender=elem.__class__, instance=elem, ar=ar) 

            elem.before_ui_save(ar) 

            elem.save(force_update=True) 

            watcher.send_update(ar.request) 

            ar.success(_("%s has been updated.") % obj2unicode(elem)) 

        else: 

            ar.success(_("%s : nothing to save.") % obj2unicode(elem)) 

 

        elem.after_ui_save(ar, watcher) 

 

        # TODO: in fact we need *either* `rows` (when this was called 

        # from a Grid) *or* `goto_instance` (when this was called from a 

        # form).  But how to find out which one is needed? 

        # if ar.edit_mode == constants.EDIT_MODE_GRID: 

        ar.set_response(rows=[ar.ah.store.row2list(ar, elem)]) 

 

 

# this is a first attempt to solve the "cannot use active fields in 

# insert window" problem.  not yet ready for use. the idea is that 

# active fields should not send a real "save" request (either POST or 

# PUT) in the background but a "validate_form" request which creates a 

# dummy instance from form content, calls it's full_clean() method to 

# have other fields filled in, and then return the modified form 

# content. Fails because the Record.phantom in ExtJS then still gets 

# lost. 

 

class ValidateForm(Action): 

    # called by active_fields 

    show_in_workflow = False 

    action_name = 'validate' 

    readonly = False 

    auto_save = False 

 

    def is_callable_from(self, caller): 

        return False 

 

    def run_from_ui(self, ar, **kw): 

        elem = ar.create_instance_from_request() 

        ar.ah.store.form2obj(ar, ar.rqdata, elem, False) 

        elem.full_clean() 

        ar.success() 

        # ar.set_response(rows=[ar.ah.store.row2list(ar, elem)]) 

        ar.goto_instance(elem) 

 

 

class SubmitDetail(SaveRow): 

    """The "Save" button of a :term:`detail window`. 

 

    Called when the OK button of a Detail Window was clicked. 

    Installed as `submit_detail` on every actor. 

 

    """ 

    icon_name = 'disk' 

    help_text = _("Save changes in this form") 

    label = _("Save") 

    action_name = ShowDetailAction.save_action_name 

 

    def is_callable_from(self, caller): 

        return isinstance(caller, ShowDetailAction) 

 

    def run_from_ui(self, ar, **kw): 

        # logger.info("20140423 SubmitDetail") 

        elem = ar.selected_rows[0] 

        # ar.form2obj_and_save(ar.rqdata, elem, False) 

        self.save_existing_instance(elem, ar) 

        ar.goto_instance(elem) 

 

 

class CreateRow(Action): 

    """Called when user edited a cell of a phantom record in a grid. 

    """ 

    sort_index = 10 

    auto_save = False 

    show_in_workflow = False 

    readonly = False 

 

    def is_callable_from(self, caller): 

        return False 

 

    def run_from_ui(self, ar, **kw): 

        elem = ar.create_instance_from_request() 

        self.save_new_instance(ar, elem) 

 

    def save_new_instance(self, ar, elem): 

        pre_ui_save.send(sender=elem.__class__, instance=elem, ar=ar) 

        elem.before_ui_save(ar) 

        elem.save(force_insert=True) 

        # yes, `on_ui_created` comes *after* save() 

        on_ui_created.send(elem, request=ar.request) 

        elem.after_ui_create(ar) 

        elem.after_ui_save(ar, None) 

        ar.success(_("%s has been created.") % obj2unicode(elem)) 

 

        if ar.actor.handle_uploaded_files is None: 

            # The `rows` can contain complex strings which cause 

            # decoding problems on the client when responding to a 

            # file upload 

            ar.set_response(rows=[ar.ah.store.row2list(ar, elem)]) 

        else: 

            # Must set text/html for file uploads, otherwise the 

            # browser adds a <PRE></PRE> tag around the AJAX response. 

            ar.set_content_type('text/html') 

 

        if ar.actor.stay_in_grid: 

            return 

            # No need to ask refresh_all since closing the window will 

            # automatically refresh the underlying window. 

 

        ar.goto_instance(elem) 

 

 

class SubmitInsert(CreateRow): 

    """Called when the OK button of an Insert Window was clicked. 

    Installed as `submit_insert` on every `dd.Model <lino.core.model.Model>`. 

    """ 

    label = _("Create") 

    action_name = None  # 'post' 

    help_text = _("Create the record and open a detail window on it") 

 

    def is_callable_from(self, caller): 

        return isinstance(caller, InsertRow) 

 

    def run_from_ui(self, ar, **kw): 

        ar.requesting_panel = None 

        # must set this to None, otherwise javascript button actions 

        # would try to refer the requesting panel which is going to be 

        # closed (this disturbs at least in ticket #219) 

        elem = ar.create_instance_from_request() 

        self.save_new_instance(ar, elem) 

        ar.set_response(close_window=True) 

 

# class SubmitInsertAndStay(SubmitInsert): 

#     sort_index = 11 

#     switch_to_detail = False 

#     action_name = 'poststay' 

#     label = _("Create without detail") 

#     help_text = _("Don't open a detail window on the new record") 

 

 

class ShowSlaveTable(Action): 

    """An action which opens a window showing the table specified when 

    instantiating the action. 

 

    """ 

    TABLE2ACTION_ATTRS = ('help_text', 'icon_name', 'label', 

                          'sort_index', 'required_roles') 

    show_in_bbar = True 

 

    def __init__(self, slave_table, **kw): 

        self.slave_table = slave_table 

        self.explicit_attribs = set(kw.keys()) 

        super(ShowSlaveTable, self).__init__(**kw) 

 

    @classmethod 

    def get_actor_label(self): 

        return self._label or self.slave_table.label 

 

    def attach_to_actor(self, actor, name): 

        if isinstance(self.slave_table, basestring): 

            T = settings.SITE.modules.resolve(self.slave_table) 

            if T is None: 

                raise Exception("No table named %s" % self.slave_table) 

            self.slave_table = T 

        for k in self.TABLE2ACTION_ATTRS: 

            if not k in self.explicit_attribs: 

                setattr(self, k, getattr(self.slave_table, k)) 

        return super(ShowSlaveTable, self).attach_to_actor(actor, name) 

 

    def run_from_ui(self, ar, **kw): 

        obj = ar.selected_rows[0] 

        sar = ar.spawn(self.slave_table, master_instance=obj) 

        js = ar.renderer.request_handler(sar) 

        ar.set_response(eval_js=js) 

 

 

class NotifyingAction(Action): 

    """An action with a generic dialog window of three fields "Summary", 

    "Description" and a checkbox "Don't send email notification". The 

    default implementation calls the request's :meth:`add_system_note 

    <lino.core.requests.BaseRequest.add_system_note>` method. 

 

    Screenshot of a notifying action: 

 

    .. image:: /images/screenshots/reception.CheckinVisitor.png 

        :scale: 50 

 

    Dialog fields: 

 

    .. attribute:: subject 

    .. attribute:: body 

    .. attribute:: silent 

 

    """ 

    custom_handler = True 

 

    parameters = dict( 

        notify_subject=models.CharField( 

            _("Summary"), blank=True, max_length=200), 

        notify_body=fields.RichTextField(_("Description"), blank=True), 

        notify_silent=models.BooleanField( 

            _("Don't send email notification"), default=False), 

    ) 

 

    params_layout = layouts.Panel(""" 

    notify_subject 

    notify_body 

    notify_silent 

    """, window_size=(50, 15)) 

 

    def get_notify_subject(self, ar, obj): 

        """ 

        Return the default value of the `notify_subject` field. 

        """ 

        return None 

 

    def get_notify_body(self, ar, obj): 

        """ 

        Return the default value of the `notify_body` field. 

        """ 

        return None 

 

    def action_param_defaults(self, ar, obj, **kw): 

        kw = super(NotifyingAction, self).action_param_defaults(ar, obj, **kw) 

        if obj is not None: 

            s = self.get_notify_subject(ar, obj) 

            if s is not None: 

                kw.update(notify_subject=s) 

            s = self.get_notify_body(ar, obj) 

            if s is not None: 

                kw.update(notify_body=s) 

        return kw 

 

    def run_from_ui(self, ar, **kw): 

        obj = ar.selected_rows[0] 

        ar.set_response(message=ar.action_param_values.notify_subject) 

        ar.set_response(refresh=True) 

        ar.set_response(success=True) 

        self.add_system_note(ar, obj) 

 

    def add_system_note(self, ar, owner, **kw): 

        #~ body = _("""%(user)s executed the following action:\n%(body)s 

        #~ """) % dict(user=ar.get_user(),body=body) 

        ar.add_system_note( 

            owner, 

            ar.action_param_values.notify_subject, 

            ar.action_param_values.notify_body, 

            ar.action_param_values.notify_silent, **kw) 

 

 

class MultipleRowAction(Action): 

    """Base class for actions that update something on every selected row. 

    """ 

    custom_handler = True 

    callable_from = (GridEdit, ShowDetailAction) 

 

    def run_on_row(self, obj, ar): 

        """This is being called on every selected row. 

        """ 

        raise NotImplemented() 

 

    def run_from_ui(self, ar, **kw): 

        ar.success(**kw) 

        n = 0 

        for obj in ar.selected_rows: 

            if not ar.response.get('success'): 

                ar.info("Aborting remaining rows") 

                break 

            ar.info("%s for %s...", str(self.label), str(obj)) 

            n += self.run_on_row(obj, ar) 

            ar.set_response(refresh_all=True) 

 

        msg = _("%d row(s) have been updated.") % n 

        ar.info(msg) 

        #~ ar.success(msg,**kw) 

 

 

class DeleteSelected(MultipleRowAction): 

    """The action used to delete the selected row(s). Automatically 

    installed on every editable actor. 

 

    """ 

 

    action_name = 'delete_selected'  # because... 

    icon_name = 'delete' 

    help_text = _("Delete this record") 

    auto_save = False 

    sort_index = 30 

    readonly = False 

    show_in_workflow = False 

    # required_roles = set([SiteUser]) 

    #~ callable_from = (GridEdit,ShowDetailAction) 

    #~ needs_selection = True 

    label = _("Delete") 

    #~ url_action_name = 'delete' 

    key = keyboard.DELETE  # (ctrl=True) 

    #~ client_side = True 

 

    def run_from_ui(self, ar, **kw): 

        objects = [] 

        for obj in ar.selected_rows: 

            objects.append(str(obj)) 

            msg = ar.actor.disable_delete(obj, ar) 

            if msg is not None: 

                ar.error(None, msg, alert=True) 

                return 

 

        def ok(ar2): 

            super(DeleteSelected, self).run_from_ui(ar, **kw) 

            ar2.success(record_deleted=True) 

            if ar2.actor.detail_action: 

                ar2.set_response( 

                    detail_handler_name=ar2.actor.detail_action.full_name()) 

 

        d = dict(num=len(objects), targets=', '.join(objects)) 

        if len(objects) == 1: 

            d.update(type=ar.actor.model._meta.verbose_name) 

        else: 

            d.update(type=ar.actor.model._meta.verbose_name_plural) 

        ar.confirm( 

            ok, 

            string_concat( 

                _("You are about to delete %(num)d %(type)s:\n" 

                  "%(targets)s") % d, 

                '\n', 

                _("Are you sure ?"))) 

 

    def run_on_row(self, obj, ar): 

        pre_ui_delete.send(sender=obj, request=ar.request) 

        obj.delete() 

        return 1 

 

 

def action(*args, **kw): 

    """Decorator to define custom actions. 

     

    The decorated function will be installed as the actions's 

    :meth:`run_from_ui <Action.run_from_ui>` method. 

 

    Same signature as :meth:`Action.__init__`. 

    In practice you'll possibly use: 

    :attr:`label <Action.label>`, 

    :attr:`help_text <Action.help_text>` and 

    :attr:`required_roles <lino.core.permissions.Permittable.required_roles>`. 

 

    """ 

    def decorator(fn): 

        assert not 'required' in kw 

        # print 20140422, fn.__name__ 

        kw.setdefault('custom_handler', True) 

        a = Action(*args, **kw) 

 

        def wrapped(ar): 

            obj = ar.selected_rows[0] 

            return fn(obj, ar) 

        a.run_from_ui = wrapped 

        return a 

    return decorator 

 

 

def get_view_permission(e): 

    from lino.utils import jsgen 

    if isinstance(e, Permittable) and not e.get_view_permission( 

            jsgen._for_user_profile): 

        return False 

    # e.g. pcsw.ClientDetail has a tab "Other", visible only to system 

    # admins but the "Other" contains a GridElement RolesByPerson 

    # which is not per se reserved for system admins.  js of normal 

    # users should not try to call on_master_changed() on it 

    parent = e.parent 

    while parent is not None: 

        if isinstance(parent, Permittable) and not parent.get_view_permission( 

                jsgen._for_user_profile): 

            return False  # bug 3 (bcss_summary) blog/2012/0927 

        parent = parent.parent 

    return True