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

# Copyright 2009-2015 Luc Saffre 

# License: BSD (see file COPYING for details) 

 

""" 

A collection of tools around :doc:`/topics/mti`. 

See the source code of :mod:`lino.test_apps.mti`. 

Certainly not perfect, but works for me. 

I wrote it mainly to solve ticket :srcref:`docs/tickets/22`. 

 

 

 

""" 

from builtins import str 

import logging 

logger = logging.getLogger(__name__) 

 

from django.db import models 

from django.db import router 

from django.db.models.deletion import Collector, DO_NOTHING 

from django.core.exceptions import ValidationError 

from django.utils.translation import ugettext_lazy as _ 

 

 

from lino.core.utils import resolve_model 

from lino.core.fields import VirtualField 

from lino.core.signals import pre_remove_child, pre_add_child  # , on_add_child 

from lino import AFTER17 

 

 

class ChildCollector(Collector): 

 

    """ 

    A Collector that does not delete the MTI parents. 

    """ 

 

    def collect(self, objs, source=None, nullable=False, collect_related=True, 

        source_attr=None, reverse_dependency=False): 

        # modified copy of django.db.models.deletion.Collector.collect() 

        # changes: 

        # DOES NOT Recursively collect concrete model's parent models. 

        # calls get_all_related_objects() with local_only=True 

 

        if self.can_fast_delete(objs): 

            self.fast_deletes.append(objs) 

            return 

        new_objs = self.add(objs, source, nullable, 

                            reverse_dependency=reverse_dependency) 

        if not new_objs: 

            return 

 

        model = new_objs[0].__class__ 

 

        if collect_related: 

            for related in model._meta.get_all_related_objects( 

                    include_hidden=True, include_proxy_eq=True, 

                    local_only=True): 

                field = related.field 

                if field.rel.on_delete == DO_NOTHING: 

                    continue 

                sub_objs = self.related_objects(related, new_objs) 

                if self.can_fast_delete(sub_objs, from_field=field): 

                    self.fast_deletes.append(sub_objs) 

                elif sub_objs: 

                    field.rel.on_delete(self, field, sub_objs, self.using) 

            for field in model._meta.virtual_fields: 

                if hasattr(field, 'bulk_related_objects'): 

                    # Its something like generic foreign key. 

                    sub_objs = field.bulk_related_objects(new_objs, self.using) 

                    self.collect(sub_objs, 

                                 source=model, 

                                 source_attr=field.rel.related_name, 

                                 nullable=True) 

 

 

class OldChildCollector(Collector): 

    # version which worked for Django 1.5 

    def collect(self, objs, source=None, nullable=False, collect_related=True, 

                source_attr=None, collect_parents=True): 

        # modified copy from original Django code 

        new_objs = self.add(objs, source, nullable) 

        if not new_objs: 

            return 

        model = new_objs[0].__class__ 

 

        if collect_related: 

            #~ for m,related in model._meta.get_all_related_objects_with_model(include_hidden=True): 

            for related in model._meta.get_all_related_objects(include_hidden=True, local_only=True): 

                field = related.field 

                if related.model._meta.auto_created: 

                    # Django 1.5.x: 

                    self.add_batch(related.model, field, new_objs) 

                else: 

                    sub_objs = self.related_objects(related, new_objs) 

                    #~ print 20130828, related.model._meta.concrete_model 

                    if not sub_objs: 

                        continue 

                    field.rel.on_delete(self, field, sub_objs, self.using) 

 

            # TODO This entire block is only needed as a special case 

            # to support cascade-deletes for GenericRelation. It 

            # should be removed/fixed when the ORM gains a proper 

            # abstraction for virtual or composite fields, and GFKs 

            # are reworked to fit into that. 

            for relation in model._meta.many_to_many: 

                if not relation.rel.through: 

                    sub_objs = relation.bulk_related_objects( 

                        new_objs, self.using) 

                    self.collect(sub_objs, 

                                 source=model, 

                                 source_attr=relation.rel.related_name, 

                                 nullable=True) 

 

 

def get_child(obj, child_model): 

    if obj.pk is not None: 

        try: 

            return child_model.objects.get(pk=obj.pk) 

        except child_model.DoesNotExist: 

            # logger.warning( 

            #     "No mti child %s in %s", 

            #     obj.pk, child_model.objects.all().query) 

            return None 

 

 

def delete_child(obj, child_model, ar=None, using=None): 

    """ 

    Delete the `child_model` instance related to `obj` without 

    deleting the parent `obj` itself. 

    """ 

    # logger.info(u"delete_child %s from %s",child_model.__name__,obj) 

    using = using or router.db_for_write(obj.__class__, instance=obj) 

    child = get_child(obj, child_model) 

    if child is None: 

        raise Exception("%s has no child in %s" % (obj, child_model.__name__)) 

 

    # msg = child.disable_delete(ar) 

    ignore_models = set() 

    # for m in models_by_base(obj.__class__): 

    #     ignore_models.remove(child_model) 

    msg = child._lino_ddh.disable_delete_on_object( 

        obj, ignore_models) 

    if msg: 

        raise ValidationError(msg) 

    # logger.debug(u"Delete child %s from %s",child_model.__name__,obj) 

    if True: 

        collector = ChildCollector(using=using) 

        collector.collect([child]) 

        # raise Exception(repr(collector.data)) 

        # model = obj.__class__ 

 

        # remove the collected MTI parents so they are not deleted 

        # (this idea didnt work: yes the parents were saved, but not 

        # their related objects). 

 

        # concrete_model = child_model._meta.concrete_model 

        # for ptr in six.itervalues(concrete_model._meta.parents): 

        #     if ptr: 

        #         # raise Exception(repr(ptr.rel.model)) 

        #         del collector.data[ptr.rel.model] 

 

    else: 

        collector = ChildCollector(using=using) 

        collector.collect([child], source=obj.__class__, 

                          nullable=True, collect_parents=False) 

    collector.delete() 

 

    #~ setattr(obj,child_model.__name__.lower(),None) 

    #~ delattr(obj,child_model.__name__.lower()) 

 

    # TODO: unchecking e.g. Company.is_courseprovider deletes the 

    # child when saving the form, but the response to the PUT returns 

    # still a True value because it works on the same memory instance 

    # (`obj`).  User sees the effect only after clicking the refresh 

    # button.  Fortunately there's no problem if the user unchecks the 

    # field and saves the form a second time. 

 

 

def insert_child(obj, child_model, full_clean=False, **attrs): 

    """Create and save an instance of `child_model` from existing `obj`. 

 

    If `full_clean` is True, call full_clean on the newly created 

    object. Default is `False` because this was the historic 

    behaviour. 

 

    """ 

    #~ assert child_model != obj.__class__ 

    #~ if child_model == obj.__class__: 

        #~ raise ValidationError( 

            #~ "A %s cannot be parent for itself" % 

            #~ obj.__class__.__name__) 

    parent_link_field = child_model._meta.parents.get(obj.__class__, None) 

    if parent_link_field is None: 

        raise ValidationError(str("A %s cannot be parent for a %s" % ( 

            obj.__class__.__name__, child_model.__name__))) 

    attrs[parent_link_field.name] = obj 

    # ~ for pm,pf in child_model._meta.parents.items(): # pm : parent model, pf : parent link field 

        #~ attrs[pf.name] = obj 

    #~ attrs["%s_ptr" % obj.__class__.__name__.lower()] = obj 

    if AFTER17: 

        fields_list = obj._meta.concrete_fields 

    else: 

        fields_list = obj._meta.fields 

    for field in fields_list: 

        attrs[field.name] = getattr(obj, field.name) 

    new_obj = child_model(**attrs) 

    #~ logger.info("20120830 insert_child %s",obj2str(new_obj)) 

 

    new_obj.save() 

    # on_add_child.send(sender=obj, child=new_obj) 

    if full_clean: 

        try: 

            new_obj.full_clean() 

            new_obj.save() 

        except ValidationError as e: 

            msg = obj.error2str(e) 

            raise ValidationError( 

                _("Problem while inserting %(child)s " 

                  "child of %(parent)s: %(message)s") % 

                dict(child=child_model.__name__, 

                     parent=obj.__class__.__name__, 

                     message=msg)) 

    return new_obj 

 

 

#~ def insert_child_and_save(obj,child_model,**attrs): 

    #~ """ 

    #~ Insert (create) and save a `child_model` instance of existing `obj`. 

    #~ """ 

    #~ obj = insert_child(obj,child_model,**attrs) 

    #~ obj.save() 

    #~ return obj 

 

 

class EnableChild(VirtualField): 

    """Rendered as a checkbox that indicates whether an mti child of the 

    given model exists. 

 

    Deprecated. Use polymorphic.Polymorphic instead. 

 

    """ 

 

    editable = True 

    #~ default = models.NOT_PROVIDED 

 

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

        raise Exception("No longer recommended. " 

                        "Use `lino.mixins.polymorphic` instead.") 

        kw.update(default=False) 

        self.child_model = child_model 

        VirtualField.__init__(self, models.BooleanField(**kw), self.has_child) 

 

    def is_enabled(self, lh): 

        """When a FormLayout is inherited by an MTI child, EnableChild fields 

        must be disabled. 

 

        """ 

        return lh.layout._datasource.model != self.child_model \ 

            and issubclass(self.child_model, lh.layout._datasource.model) 

 

    def attach_to_model(self, model, name): 

        self.child_model = resolve_model( 

            self.child_model, model._meta.app_label) 

        VirtualField.attach_to_model(self, model, name) 

 

    def has_child(self, obj, request=None): 

        """Returns True if `obj` has an MTI child in `self.child_model`.  The 

        optional 2nd argument `request` (passed from 

        `VirtualField.value_from_object`) is ignored. 

 

        """ 

        try: 

            getattr(obj, self.child_model.__name__.lower()) 

            #~ child = getattr(obj,self.child_model.__name__.lower()) 

            #~ if child is None: return False 

            #~ print 20120531, repr(child) 

            #~ self.child_model.objects.get(pk=obj.pk) 

        except self.child_model.DoesNotExist: 

            return False 

        return True 

 

    def set_value_in_object(self, ar, obj, v): 

        if self.has_child(obj): 

            #~ logger.debug('set_value_in_object : %s has child %s', 

                #~ obj.__class__.__name__,self.child_model.__name__) 

            # child exists, delete it if it may not 

            if not v: 

                if ar is not None: 

                    pre_remove_child.send( 

                        sender=obj, request=ar.request, 

                        child=self.child_model) 

                delete_child(obj, self.child_model, ar) 

        else: 

            #~ logger.debug('set_value_in_object : %s has no child %s', 

                #~ obj.__class__.__name__,self.child_model.__name__) 

            if v: 

                # child doesn't exist. insert if it should 

                if ar is not None: 

                    pre_add_child.send( 

                        sender=obj, request=ar.request, 

                        child=self.child_model) 

                insert_child(obj, self.child_model, full_clean=True) 

 

 

from lino.utils.dpy import create_mti_child as create_child